From 6acb0f5dca90050942465d5e459568de0729cf75 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 19 Feb 2025 13:42:36 -0800 Subject: [PATCH 001/776] chore: enforce that account only owns one of 'singleton items' (#969) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/969 --- src/services/inventoryService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 65ccd2d0..de73a2ef 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -830,6 +830,9 @@ const addCrewShip = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { + if (inventory.CrewShips.length != 0) { + throw new Error("refusing to add CrewShip because account already has one"); + } const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; inventoryChanges.CrewShips ??= []; (inventoryChanges.CrewShips as object[]).push(inventory.CrewShips[index].toJSON()); @@ -841,6 +844,9 @@ const addCrewShipHarness = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { + if (inventory.CrewShips.length != 0) { + throw new Error("refusing to add CrewShipHarness because account already has one"); + } const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; inventoryChanges.CrewShipHarnesses ??= []; (inventoryChanges.CrewShipHarnesses as object[]).push(inventory.CrewShipHarnesses[index].toJSON()); From dee302c996545fa2e534dba22880993ae84d7222 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 19 Feb 2025 13:53:21 -0800 Subject: [PATCH 002/776] chore: handle motorcycle in addItems (#970) Closes #968 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/970 --- src/services/inventoryService.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index de73a2ef..c2df25d0 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -516,6 +516,11 @@ export const addItem = async ( } }; } + case "Vehicles": + if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") { + return { InventoryChanges: addMotorcycle(inventory, typeName) }; + } + break; } break; } @@ -853,6 +858,20 @@ const addCrewShipHarness = ( return inventoryChanges; }; +const addMotorcycle = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + if (inventory.Motorcycles.length != 0) { + throw new Error("refusing to add Motorcycle because account already has one"); + } + const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; + inventoryChanges.Motorcycles ??= []; + (inventoryChanges.Motorcycles as object[]).push(inventory.Motorcycles[index].toJSON()); + return inventoryChanges; +}; + //TODO: wrong id is not erroring export const addGearExpByCategory = ( inventory: TInventoryDatabaseDocument, From ca4017ad1eed60fb719c0caa7c9c22ebe4e82496 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 19 Feb 2025 14:07:28 -0800 Subject: [PATCH 003/776] chore: typings (#971) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/971 --- src/services/inventoryService.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index c2df25d0..f7720a35 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -840,7 +840,7 @@ const addCrewShip = ( } const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; inventoryChanges.CrewShips ??= []; - (inventoryChanges.CrewShips as object[]).push(inventory.CrewShips[index].toJSON()); + (inventoryChanges.CrewShips as IEquipmentClient[]).push(inventory.CrewShips[index].toJSON()); return inventoryChanges; }; @@ -854,7 +854,9 @@ const addCrewShipHarness = ( } const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; inventoryChanges.CrewShipHarnesses ??= []; - (inventoryChanges.CrewShipHarnesses as object[]).push(inventory.CrewShipHarnesses[index].toJSON()); + (inventoryChanges.CrewShipHarnesses as IEquipmentClient[]).push( + inventory.CrewShipHarnesses[index].toJSON() + ); return inventoryChanges; }; @@ -868,7 +870,7 @@ const addMotorcycle = ( } const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; inventoryChanges.Motorcycles ??= []; - (inventoryChanges.Motorcycles as object[]).push(inventory.Motorcycles[index].toJSON()); + (inventoryChanges.Motorcycles as IEquipmentClient[]).push(inventory.Motorcycles[index].toJSON()); return inventoryChanges; }; From b551563681591b8e6b25799791489c5d441ab6f4 Mon Sep 17 00:00:00 2001 From: CyberVenom Date: Wed, 19 Feb 2025 14:09:02 -0800 Subject: [PATCH 004/776] fix: save settings when accepting trade policy. (#966) ![image.png](/attachments/b9954b5f-5ece-4803-b728-548ca2320fdf) Co-authored-by: Kenya-DK Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/966 Co-authored-by: CyberVenom Co-committed-by: CyberVenom --- src/controllers/api/saveSettingsController.ts | 22 +++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 src/controllers/api/saveSettingsController.ts diff --git a/src/controllers/api/saveSettingsController.ts b/src/controllers/api/saveSettingsController.ts new file mode 100644 index 00000000..72bf8bfa --- /dev/null +++ b/src/controllers/api/saveSettingsController.ts @@ -0,0 +1,22 @@ +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { RequestHandler } from "express"; +import { ISettings } from "../../types/inventoryTypes/inventoryTypes"; + +interface ISaveSettingsRequest { + Settings: ISettings; +} + +const saveSettingsController: RequestHandler = async (req, res): Promise => { + const accountId = await getAccountIdForRequest(req); + + const settingResults = getJSONfromString(String(req.body)); + + const inventory = await getInventory(accountId); + inventory.Settings = Object.assign(inventory.Settings, settingResults.Settings); + await inventory.save(); + res.json(inventory.Settings); +}; + +export { saveSettingsController }; diff --git a/src/routes/api.ts b/src/routes/api.ts index d42a9f82..7ad9e5a6 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -86,6 +86,7 @@ import { updateQuestController } from "@/src/controllers/api/updateQuestControll import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController"; import { updateThemeController } from "../controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; +import { saveSettingsController } from "../controllers/api/saveSettingsController"; const apiRouter = express.Router(); @@ -182,5 +183,6 @@ apiRouter.post("/updateQuest.php", updateQuestController); apiRouter.post("/updateSession.php", updateSessionPostController); apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/upgrades.php", upgradesController); +apiRouter.post("/saveSettings.php", saveSettingsController); export { apiRouter }; From fb8d176fbef026116a4c3414d825c5e0a5ec3d86 Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:09:47 -0800 Subject: [PATCH 005/776] fix(webui): quest cheats (#965) Completing Quests via the webui will now also award the quest's items and mails. Also fixes doubly adding key chain items. A few items will not be added, as it is currently impossible to determine the item category by path for these items. This will be fixed soon. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/965 Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> --- .../giveKeyChainTriggeredItemsController.ts | 25 +---- .../giveKeyChainTriggeredMessageController.ts | 23 +--- .../custom/manageQuestsController.ts | 68 ++++++++---- src/models/inventoryModels/inventoryModel.ts | 1 + src/services/inventoryService.ts | 8 +- src/services/questService.ts | 101 ++++++++++++++++++ 6 files changed, 161 insertions(+), 65 deletions(-) diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts index ef1e4700..8e391b01 100644 --- a/src/controllers/api/giveKeyChainTriggeredItemsController.ts +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -1,34 +1,19 @@ import { RequestHandler } from "express"; -import { isEmptyObject, parseString } from "@/src/helpers/general"; +import { parseString } from "@/src/helpers/general"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { addKeyChainItems, getInventory } from "@/src/services/inventoryService"; +import { getInventory } from "@/src/services/inventoryService"; import { IGroup } from "@/src/types/loginTypes"; -import { updateQuestStage } from "@/src/services/questService"; +import { giveKeyChainItem } from "@/src/services/questService"; export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); const keyChainInfo = getJSONfromString((req.body as string).toString()); const inventory = await getInventory(accountId); - const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); - - if (isEmptyObject(inventoryChanges)) { - throw new Error("inventory changes was empty after getting keychain items: should not happen"); - } - // items were added: update quest stage's i (item was given) - updateQuestStage(inventory, keyChainInfo, { i: true }); - + const inventoryChanges = giveKeyChainItem(inventory, keyChainInfo); await inventory.save(); - res.send(inventoryChanges); - //TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory - /* - some items are added or removed (not sure) to the wishlist, in that case a - WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"], - is added to the response, need to determine for which items this is the case and what purpose this has. - */ - //{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0} - //{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]} + res.send(inventoryChanges); }; export interface IKeyChainRequest { diff --git a/src/controllers/api/giveKeyChainTriggeredMessageController.ts b/src/controllers/api/giveKeyChainTriggeredMessageController.ts index 0f699d42..dec4b8a1 100644 --- a/src/controllers/api/giveKeyChainTriggeredMessageController.ts +++ b/src/controllers/api/giveKeyChainTriggeredMessageController.ts @@ -1,34 +1,15 @@ import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; -import { IMessage } from "@/src/models/inboxModel"; -import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; -import { getKeyChainMessage } from "@/src/services/itemDataService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { updateQuestStage } from "@/src/services/questService"; +import { giveKeyChainMessage } from "@/src/services/questService"; import { RequestHandler } from "express"; export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest; - const keyChainMessage = getKeyChainMessage(keyChainInfo); - - const message = { - sndr: keyChainMessage.sender, - msg: keyChainMessage.body, - sub: keyChainMessage.title, - att: keyChainMessage.attachments.length > 0 ? keyChainMessage.attachments : undefined, - countedAtt: keyChainMessage.countedAttachments.length > 0 ? keyChainMessage.countedAttachments : undefined, - icon: keyChainMessage.icon ?? "", - transmission: keyChainMessage.transmission ?? "", - highPriority: keyChainMessage.highPriority ?? false, - r: false - } satisfies IMessage; - - await createMessage(accountId, [message]); - const inventory = await getInventory(accountId, "QuestKeys"); - updateQuestStage(inventory, keyChainInfo, { m: true }); + await giveKeyChainMessage(inventory, accountId, keyChainInfo); await inventory.save(); res.send(1); diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 504b4951..49283bf3 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -1,8 +1,7 @@ import { addString } from "@/src/controllers/api/inventoryController"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addQuestKey, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService"; -import { IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; +import { addQuestKey, completeQuest, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { ExportKeys } from "warframe-public-export-plus"; @@ -23,7 +22,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => { allQuestKeys.push(k); } } - const inventory = await getInventory(accountId, "QuestKeys NodeIntrosCompleted"); + const inventory = await getInventory(accountId); switch (operation) { case "updateKey": { @@ -40,21 +39,31 @@ export const manageQuestsController: RequestHandler = async (req, res) => { case "completeAll": { logger.info("completing all quests.."); for (const questKey of allQuestKeys) { - const chainStageTotal = ExportKeys[questKey].chainStages?.length ?? 0; - const Progress = Array(chainStageTotal).fill({ c: 0, i: true, m: true, b: [] } satisfies IQuestStage); - const inventoryQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); - if (inventoryQuestKey) { - inventoryQuestKey.Completed = true; - inventoryQuestKey.Progress = Progress; - continue; + try { + await completeQuest(inventory, questKey); + } catch (error) { + if (error instanceof Error) { + logger.error( + `Something went wrong completing quest ${questKey}, probably could not add some item` + ); + logger.error(error.message); + } } - addQuestKey(inventory, { ItemType: questKey, Completed: true, unlock: true, Progress: Progress }); - } - inventory.ArchwingEnabled = true; - inventory.ActiveQuest = ""; - // Skip "Watch The Maker" - addString(inventory.NodeIntrosCompleted, "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level"); + //Skip "Watch The Maker" + if (questKey === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") { + addString( + inventory.NodeIntrosCompleted, + "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level" + ); + } + + if (questKey === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") { + inventory.ArchwingEnabled = true; + } + } + + inventory.ActiveQuest = ""; break; } case "ResetAll": { @@ -63,14 +72,37 @@ export const manageQuestsController: RequestHandler = async (req, res) => { questKey.Completed = false; questKey.Progress = []; } + inventory.ActiveQuest = ""; break; } case "completeAllUnlocked": { logger.info("completing all unlocked quests.."); for (const questKey of inventory.QuestKeys) { - //if (!questKey.unlock) { continue; } - questKey.Completed = true; + console.log("size of questkeys", inventory.QuestKeys.length); + try { + await completeQuest(inventory, questKey.ItemType); + } catch (error) { + if (error instanceof Error) { + logger.error( + `Something went wrong completing quest ${questKey.ItemType}, probably could not add some item` + ); + logger.error(error.message); + } + } + + //Skip "Watch The Maker" + if (questKey.ItemType === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") { + addString( + inventory.NodeIntrosCompleted, + "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level" + ); + } + + if (questKey.ItemType === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") { + inventory.ArchwingEnabled = true; + } } + inventory.ActiveQuest = ""; break; } } diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5848600e..fe651349 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1249,6 +1249,7 @@ export type InventoryDocumentProps = { KahlLoadOuts: Types.DocumentArray; PendingRecipes: Types.DocumentArray; WeaponSkins: Types.DocumentArray; + QuestKeys: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index f7720a35..8780e82f 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -367,7 +367,7 @@ export const addItem = async ( }; } if (typeName in ExportKeys) { - // Note: "/Lotus/Types/Keys/" contains some EmailItems and ShipFeatureItems + // Note: "/Lotus/Types/Keys/" contains some EmailItems inventory.QuestKeys.push({ ItemType: typeName }); return { InventoryChanges: { @@ -524,9 +524,7 @@ export const addItem = async ( } break; } - const errorMessage = `unable to add item: ${typeName}`; - logger.error(errorMessage); - throw new Error(errorMessage); + throw new Error(`unable to add item: ${typeName}`); }; export const addItems = async ( @@ -1183,7 +1181,5 @@ export const addKeyChainItems = async ( combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges); } - await addItems(inventory, nonStoreItems); - return inventoryChanges; }; diff --git a/src/services/questService.ts b/src/services/questService.ts index 75316763..e51eb70a 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -1,8 +1,14 @@ import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { isEmptyObject } from "@/src/helpers/general"; +import { IMessage } from "@/src/models/inboxModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { createMessage } from "@/src/services/inboxService"; +import { addKeyChainItems } from "@/src/services/inventoryService"; +import { getKeyChainMessage } from "@/src/services/itemDataService"; import { IInventoryDatabase, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { HydratedDocument } from "mongoose"; +import { ExportKeys } from "warframe-public-export-plus"; export interface IUpdateQuestRequest { QuestKeys: Omit[]; @@ -70,3 +76,98 @@ export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQu } inventory.QuestKeys.push(questKey); }; + +export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string) => { + const chainStages = ExportKeys[questKey]?.chainStages; + + if (!chainStages) { + throw new Error(`Quest ${questKey} does not contain chain stages`); + } + + const chainStageTotal = ExportKeys[questKey].chainStages?.length ?? 0; + + const existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); + + if (existingQuestKey?.Completed) { + return; + } + const Progress = Array(chainStageTotal).fill({ + c: 0, + i: false, + m: false, + b: [] + } satisfies IQuestStage); + + const completedQuestKey: IQuestKeyDatabase = { + ItemType: questKey, + Completed: true, + unlock: true, + Progress: Progress, + CompletionDate: new Date() + }; + + //overwrite current quest progress, might lead to multiple quest item rewards + if (existingQuestKey) { + existingQuestKey.overwrite(completedQuestKey); + //Object.assign(existingQuestKey, completedQuestKey); + } else { + addQuestKey(inventory, completedQuestKey); + } + + for (let i = 0; i < chainStageTotal; i++) { + if (chainStages[i].itemsToGiveWhenTriggered.length > 0) { + await giveKeyChainItem(inventory, { KeyChain: questKey, ChainStage: i }); + } + + if (chainStages[i].messageToSendWhenTriggered) { + await giveKeyChainMessage(inventory, inventory.accountOwnerId.toString(), { + KeyChain: questKey, + ChainStage: i + }); + } + } + //TODO: handle quest completions +}; + +export const giveKeyChainItem = async (inventory: TInventoryDatabaseDocument, keyChainInfo: IKeyChainRequest) => { + const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); + + if (isEmptyObject(inventoryChanges)) { + throw new Error("inventory changes was empty after getting keychain items: should not happen"); + } + // items were added: update quest stage's i (item was given) + updateQuestStage(inventory, keyChainInfo, { i: true }); + + //TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory + /* + some items are added or removed (not sure) to the wishlist, in that case a + WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"], + is added to the response, need to determine for which items this is the case and what purpose this has. + */ + //{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0} + //{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]} +}; + +export const giveKeyChainMessage = async ( + inventory: TInventoryDatabaseDocument, + accountId: string, + keyChainInfo: IKeyChainRequest +) => { + const keyChainMessage = getKeyChainMessage(keyChainInfo); + + const message = { + sndr: keyChainMessage.sender, + msg: keyChainMessage.body, + sub: keyChainMessage.title, + att: keyChainMessage.attachments.length > 0 ? keyChainMessage.attachments : undefined, + countedAtt: keyChainMessage.countedAttachments.length > 0 ? keyChainMessage.countedAttachments : undefined, + icon: keyChainMessage.icon ?? "", + transmission: keyChainMessage.transmission ?? "", + highPriority: keyChainMessage.highPriority ?? false, + r: false + } satisfies IMessage; + + await createMessage(accountId, [message]); + + updateQuestStage(inventory, keyChainInfo, { m: true }); +}; From b4e780baa33528765cb79ef95dcc3c12eea30f45 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Feb 2025 02:57:23 -0800 Subject: [PATCH 006/776] fix: save LoreFragmentScans (#974) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/974 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 5 +++++ src/types/requestTypes.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 0cf0a0c7..b98f7a4c 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -191,6 +191,11 @@ export const addMissionInventoryUpdates = ( }); break; } + case "LoreFragmentScans": + value.forEach(x => { + inventory.LoreFragmentScans.push(x); + }); + break; case "SyndicateId": { inventory.CompletedSyndicates.push(value); break; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 01302bdc..e7706d01 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -13,7 +13,8 @@ import { IFusionTreasure, ICustomMarkers, IPlayerSkills, - IQuestKeyDatabase + IQuestKeyDatabase, + ILoreFragmentScan } from "./inventoryTypes/inventoryTypes"; export interface IThemeUpdateRequest { @@ -85,6 +86,7 @@ export type IMissionInventoryUpdateRequest = { FocusXpIncreases?: number[]; PlayerSkillGains: IPlayerSkills; CustomMarkers?: ICustomMarkers[]; + LoreFragmentScans?: ILoreFragmentScan[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From ac6ac19199769ba07fc4a1f2dbc77ef18b233891 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Feb 2025 02:58:44 -0800 Subject: [PATCH 007/776] chore: properly type equipment in IInventoryChanges (#973) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/973 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 34 +++++++++++--------------------- src/types/purchaseTypes.ts | 5 ++++- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 8780e82f..c43e1d1c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -234,7 +234,7 @@ export const addItem = async ( InventoryChanges: { Ships: [ { - ItemId: { $oid: oid }, + ItemId: { $oid: oid.toString() }, ItemType: typeName } ] @@ -499,7 +499,7 @@ export const addItem = async ( const horseIndex = inventory.Horses.push({ ItemType: typeName }); return { InventoryChanges: { - Horses: inventory.Horses[horseIndex - 1].toJSON() + Horses: [inventory.Horses[horseIndex - 1].toJSON()] } }; } @@ -572,9 +572,7 @@ export const addSentinel = ( addMods(inventory, modsToGive); const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0 }) - 1; inventoryChanges.Sentinels ??= []; - (inventoryChanges.Sentinels as IEquipmentClient[]).push( - inventory.Sentinels[sentinelIndex].toJSON() - ); + inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON()); return inventoryChanges; }; @@ -586,9 +584,7 @@ export const addSentinelWeapon = ( ): void => { const index = inventory.SentinelWeapons.push({ ItemType: typeName, XP: 0 }) - 1; inventoryChanges.SentinelWeapons ??= []; - (inventoryChanges.SentinelWeapons as IEquipmentClient[]).push( - inventory.SentinelWeapons[index].toJSON() - ); + inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON()); }; export const addPowerSuit = ( @@ -604,7 +600,7 @@ export const addPowerSuit = ( } const suitIndex = inventory.Suits.push({ ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; inventoryChanges.Suits ??= []; - (inventoryChanges.Suits as IEquipmentClient[]).push(inventory.Suits[suitIndex].toJSON()); + inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON()); return inventoryChanges; }; @@ -621,7 +617,7 @@ export const addMechSuit = ( } const suitIndex = inventory.MechSuits.push({ ItemType: mechsuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; inventoryChanges.MechSuits ??= []; - (inventoryChanges.MechSuits as IEquipmentClient[]).push(inventory.MechSuits[suitIndex].toJSON()); + inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); return inventoryChanges; }; @@ -642,9 +638,7 @@ export const addSpecialItem = ( XP: 0 }) - 1; inventoryChanges.SpecialItems ??= []; - (inventoryChanges.SpecialItems as IEquipmentClient[]).push( - inventory.SpecialItems[specialItemIndex].toJSON() - ); + inventoryChanges.SpecialItems.push(inventory.SpecialItems[specialItemIndex].toJSON()); }; export const addSpaceSuit = ( @@ -654,9 +648,7 @@ export const addSpaceSuit = ( ): IInventoryChanges => { const suitIndex = inventory.SpaceSuits.push({ ItemType: spacesuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; inventoryChanges.SpaceSuits ??= []; - (inventoryChanges.SpaceSuits as IEquipmentClient[]).push( - inventory.SpaceSuits[suitIndex].toJSON() - ); + inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON()); return inventoryChanges; }; @@ -798,7 +790,7 @@ export const addEquipment = ( }) - 1; inventoryChanges[category] ??= []; - (inventoryChanges[category] as IEquipmentClient[]).push(inventory[category][index].toJSON()); + inventoryChanges[category].push(inventory[category][index].toJSON()); return inventoryChanges; }; @@ -838,7 +830,7 @@ const addCrewShip = ( } const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; inventoryChanges.CrewShips ??= []; - (inventoryChanges.CrewShips as IEquipmentClient[]).push(inventory.CrewShips[index].toJSON()); + inventoryChanges.CrewShips.push(inventory.CrewShips[index].toJSON()); return inventoryChanges; }; @@ -852,9 +844,7 @@ const addCrewShipHarness = ( } const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; inventoryChanges.CrewShipHarnesses ??= []; - (inventoryChanges.CrewShipHarnesses as IEquipmentClient[]).push( - inventory.CrewShipHarnesses[index].toJSON() - ); + inventoryChanges.CrewShipHarnesses.push(inventory.CrewShipHarnesses[index].toJSON()); return inventoryChanges; }; @@ -868,7 +858,7 @@ const addMotorcycle = ( } const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; inventoryChanges.Motorcycles ??= []; - (inventoryChanges.Motorcycles as IEquipmentClient[]).push(inventory.Motorcycles[index].toJSON()); + inventoryChanges.Motorcycles.push(inventory.Motorcycles[index].toJSON()); return inventoryChanges; }; diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 94e1d197..8dcdb4e8 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -1,4 +1,5 @@ -import { IInfestedFoundryClient } from "./inventoryTypes/inventoryTypes"; +import { IEquipmentClient } from "./inventoryTypes/commonInventoryTypes"; +import { IInfestedFoundryClient, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { PurchaseParams: IPurchaseParams; @@ -29,6 +30,8 @@ export interface ICurrencyChanges { export type IInventoryChanges = { [_ in SlotNames]?: IBinChanges; +} & { + [_ in TEquipmentKey]?: IEquipmentClient[]; } & ICurrencyChanges & { InfestedFoundry?: IInfestedFoundryClient } & Record< string, IBinChanges | number | object[] | IInfestedFoundryClient From 815d18623e80fa931d023cc06f48277b392114a7 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Feb 2025 02:58:57 -0800 Subject: [PATCH 008/776] chore: generate inventory equipment types from equipmentKeys (#972) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/972 Co-authored-by: Sainan Co-committed-by: Sainan --- src/types/inventoryTypes/inventoryTypes.ts | 102 +++++++-------------- 1 file changed, 32 insertions(+), 70 deletions(-) diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e27df725..fab41e4e 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -12,30 +12,35 @@ import { IOperatorConfigDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +export type InventoryDatabaseEquipment = { + [_ in TEquipmentKey]: IEquipmentDatabase[]; +}; + export interface IInventoryDatabase extends Omit< - IInventoryClient, - | "TrainingDate" - | "LoadOutPresets" - | "Mailbox" - | "GuildId" - | "PendingRecipes" - | "Created" - | "QuestKeys" - | "BlessingCooldown" - | "Ships" - | "WeaponSkins" - | "Upgrades" - | "CrewShipSalvagedWeaponSkins" - | "CrewShipWeaponSkins" - | "AdultOperatorLoadOuts" - | "OperatorLoadOuts" - | "KahlLoadOuts" - | "InfestedFoundry" - | "DialogueHistory" - | "KubrowPetEggs" - | TEquipmentKey - > { + IInventoryClient, + | "TrainingDate" + | "LoadOutPresets" + | "Mailbox" + | "GuildId" + | "PendingRecipes" + | "Created" + | "QuestKeys" + | "BlessingCooldown" + | "Ships" + | "WeaponSkins" + | "Upgrades" + | "CrewShipSalvagedWeaponSkins" + | "CrewShipWeaponSkins" + | "AdultOperatorLoadOuts" + | "OperatorLoadOuts" + | "KahlLoadOuts" + | "InfestedFoundry" + | "DialogueHistory" + | "KubrowPetEggs" + | TEquipmentKey + >, + InventoryDatabaseEquipment { accountOwnerId: Types.ObjectId; Created: Date; TrainingDate: Date; @@ -56,30 +61,6 @@ export interface IInventoryDatabase InfestedFoundry?: IInfestedFoundryDatabase; DialogueHistory?: IDialogueHistoryDatabase; KubrowPetEggs?: IKubrowPetEggDatabase[]; - - Suits: IEquipmentDatabase[]; - LongGuns: IEquipmentDatabase[]; - Pistols: IEquipmentDatabase[]; - Melee: IEquipmentDatabase[]; - SpecialItems: IEquipmentDatabase[]; - Sentinels: IEquipmentDatabase[]; - SentinelWeapons: IEquipmentDatabase[]; - SpaceSuits: IEquipmentDatabase[]; - SpaceGuns: IEquipmentDatabase[]; - SpaceMelee: IEquipmentDatabase[]; - Hoverboards: IEquipmentDatabase[]; - OperatorAmps: IEquipmentDatabase[]; - MoaPets: IEquipmentDatabase[]; - Scoops: IEquipmentDatabase[]; - Horses: IEquipmentDatabase[]; - DrifterGuns: IEquipmentDatabase[]; - DrifterMelee: IEquipmentDatabase[]; - Motorcycles: IEquipmentDatabase[]; - CrewShips: IEquipmentDatabase[]; - DataKnives: IEquipmentDatabase[]; - MechSuits: IEquipmentDatabase[]; - CrewShipHarnesses: IEquipmentDatabase[]; - KubrowPets: IEquipmentDatabase[]; } export interface IQuestKeyDatabase { @@ -179,30 +160,11 @@ export interface IDailyAffiliations { DailyAffiliationHex: number; } -export interface IInventoryClient extends IDailyAffiliations { - Suits: IEquipmentClient[]; - LongGuns: IEquipmentClient[]; - Pistols: IEquipmentClient[]; - Melee: IEquipmentClient[]; - SpecialItems: IEquipmentClient[]; - Sentinels: IEquipmentClient[]; - SentinelWeapons: IEquipmentClient[]; - SpaceSuits: IEquipmentClient[]; - SpaceGuns: IEquipmentClient[]; - SpaceMelee: IEquipmentClient[]; - Hoverboards: IEquipmentClient[]; - OperatorAmps: IEquipmentClient[]; - MoaPets: IEquipmentClient[]; - Scoops: IEquipmentClient[]; - Horses: IEquipmentClient[]; - DrifterGuns: IEquipmentClient[]; - DrifterMelee: IEquipmentClient[]; - Motorcycles: IEquipmentClient[]; - CrewShips: IEquipmentClient[]; - DataKnives: IEquipmentClient[]; - MechSuits: IEquipmentClient[]; - CrewShipHarnesses: IEquipmentClient[]; - KubrowPets: IEquipmentClient[]; +export type InventoryClientEquipment = { + [_ in TEquipmentKey]: IEquipmentClient[]; +}; + +export interface IInventoryClient extends IDailyAffiliations, InventoryClientEquipment { AdultOperatorLoadOuts: IOperatorConfigClient[]; OperatorLoadOuts: IOperatorConfigClient[]; KahlLoadOuts: IOperatorConfigClient[]; From 78548a2ebe057be22e128cdf5041c7045e701f4d Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Thu, 20 Feb 2025 06:16:40 -0800 Subject: [PATCH 009/776] chore: cleanup config (#979) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/979 Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> --- config.json.example | 3 +++ src/controllers/api/loginController.ts | 11 +++++------ src/services/configService.ts | 3 +++ static/fixed_responses/login_static.ts | 27 -------------------------- 4 files changed, 11 insertions(+), 33 deletions(-) delete mode 100644 static/fixed_responses/login_static.ts diff --git a/config.json.example b/config.json.example index 2bdc035a..ec776374 100644 --- a/config.json.example +++ b/config.json.example @@ -5,6 +5,9 @@ "level": "trace" }, "myAddress": "localhost", + "hubAddress": "https://localhost/api/", + "platformCDNs": ["https://localhost/"], + "NRS": ["localhost"], "httpPort": 80, "httpsPort": 443, "administratorNames": [], diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 00f1380c..d57fd7e1 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -6,7 +6,6 @@ import { buildConfig } from "@/src/services/buildConfigService"; import { Account } from "@/src/models/loginModel"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; -import { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static"; import { logger } from "@/src/utils/logger"; export const loginController: RequestHandler = async (request, response) => { @@ -84,12 +83,12 @@ const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): ConsentNeeded: account.ConsentNeeded, TrackedSettings: account.TrackedSettings, Nonce: account.Nonce, - Groups: groups, - platformCDNs: platformCDNs, - NRS: [config.myAddress], - DTLS: DTLS, + Groups: [], IRC: config.myIrcAddresses ?? [config.myAddress], - HUB: HUB, + platformCDNs: config.platformCDNs, + HUB: config.hubAddress, + NRS: config.NRS, + DTLS: 99, BuildLabel: buildLabel, MatchmakingBuildId: buildConfig.matchmakingBuildId }; diff --git a/src/services/configService.ts b/src/services/configService.ts index 2312727b..d046a1ee 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -33,6 +33,9 @@ interface IConfig { httpPort?: number; httpsPort?: number; myIrcAddresses?: string[]; + platformCDNs: string[]; + hubAddress: string; + NRS: string[]; administratorNames?: string[] | string; autoCreateAccount?: boolean; skipTutorial?: boolean; diff --git a/static/fixed_responses/login_static.ts b/static/fixed_responses/login_static.ts deleted file mode 100644 index d5e5ea1a..00000000 --- a/static/fixed_responses/login_static.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IGroup } from "@/src/types/loginTypes"; - -export const groups: IGroup[] = [ - { - experiment: "InitiatePage", - experimentGroup: "initiate_page_no_video" - }, - { experiment: "ChatQAChannel", experimentGroup: "control" }, - { - experiment: "MarketSearchRecommendations", - experimentGroup: "premium_credit_purchases_14_days" - }, - { experiment: "SurveyLocation", experimentGroup: "EXIT" }, - { experiment: "GamesightAB", experimentGroup: "a" } -]; - -export const platformCDNs = [ - "https://content.warframe.com/", - "https://content-xb1.warframe.com/", - "https://content-ps4.warframe.com/", - "https://content-swi.warframe.com/", - "https://content-mob.warframe.com/" -]; - -export const DTLS = 99; - -export const HUB = "https://arbiter.warframe.com/api/"; From 4d7b3b543b19c050fcd5e6e23a3af40c78c44e5e Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Feb 2025 08:29:42 +0100 Subject: [PATCH 010/776] fix: typings not matching reality --- src/services/configService.ts | 6 +++--- src/types/loginTypes.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/configService.ts b/src/services/configService.ts index d046a1ee..bb4d5c96 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -33,9 +33,9 @@ interface IConfig { httpPort?: number; httpsPort?: number; myIrcAddresses?: string[]; - platformCDNs: string[]; - hubAddress: string; - NRS: string[]; + platformCDNs?: string[]; + hubAddress?: string; + NRS?: string[]; administratorNames?: string[] | string; autoCreateAccount?: boolean; skipTutorial?: boolean; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 728fde52..687d611e 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -39,11 +39,11 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons { Groups: IGroup[]; BuildLabel: string; MatchmakingBuildId: string; - platformCDNs: string[]; - NRS: string[]; + platformCDNs?: string[]; + NRS?: string[]; DTLS: number; IRC: string[]; - HUB: string; + HUB?: string; } export interface IGroup { From a259afe912e0066577ffb1c0447abf643fb5cf98 Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Fri, 21 Feb 2025 05:30:13 -0800 Subject: [PATCH 011/776] feat(webui): give all quests (#981) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/981 Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> --- src/controllers/custom/manageQuestsController.ts | 11 +++++++++-- static/webui/index.html | 10 ++++++---- static/webui/translations/en.js | 9 +++++---- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 49283bf3..edf1feae 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -13,7 +13,8 @@ export const manageQuestsController: RequestHandler = async (req, res) => { | "completeAll" | "ResetAll" | "completeAllUnlocked" - | "updateKey"; + | "updateKey" + | "giveAll"; const questKeyUpdate = req.body as IUpdateQuestRequest["QuestKeys"]; const allQuestKeys: string[] = []; @@ -71,6 +72,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => { for (const questKey of inventory.QuestKeys) { questKey.Completed = false; questKey.Progress = []; + questKey.CompletionDate = undefined; } inventory.ActiveQuest = ""; break; @@ -78,7 +80,6 @@ export const manageQuestsController: RequestHandler = async (req, res) => { case "completeAllUnlocked": { logger.info("completing all unlocked quests.."); for (const questKey of inventory.QuestKeys) { - console.log("size of questkeys", inventory.QuestKeys.length); try { await completeQuest(inventory, questKey.ItemType); } catch (error) { @@ -105,6 +106,12 @@ export const manageQuestsController: RequestHandler = async (req, res) => { inventory.ActiveQuest = ""; break; } + case "giveAll": { + for (const questKey of allQuestKeys) { + addQuestKey(inventory, { ItemType: questKey }); + } + break; + } } await inventory.save(); diff --git a/static/webui/index.html b/static/webui/index.html index 6af03e3c..081ca3d2 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -539,10 +539,12 @@
- - - - + + + + + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 70203a0f..77cadccc 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -118,10 +118,11 @@ dict = { cheats_changeButton: `Change`, cheats_none: `None`, cheats_quests: `Quests`, - cheats_quests_UnlockAll: `Unlock All Quests`, - cheats_quests_CompleteAll: `Complete All Quests`, - cheats_quests_CompleteAllUnlocked: `Complete All Unlocked Quests`, - cheats_quests_ResetAll: `Reset All Quests`, + cheats_quests_unlockAll: `Unlock All Quests`, + cheats_quests_completeAll: `Complete All Quests`, + cheats_quests_completeAllUnlocked: `Complete All Unlocked Quests`, + cheats_quests_resetAll: `Reset All Quests`, + cheats_quests_giveAll: `Give All Quests`, import_importNote: `You can provide a full or partial inventory response (client respresentation) here. All fields that are supported by the importer will be overwritten in your account.`, import_submit: `Submit` }; From eb56442d6319940a11edeb270cb5608a34dfb679 Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Fri, 21 Feb 2025 06:32:05 -0800 Subject: [PATCH 012/776] fix: junction rewards (#982) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/982 Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 16 +++ src/types/commonTypes.ts | 9 ++ static/fixed_responses/junctionRewards.json | 120 ++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 static/fixed_responses/junctionRewards.json diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b98f7a4c..766518e2 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -26,6 +26,8 @@ import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService"; import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import junctionRewards from "@/static/fixed_responses/junctionRewards.json"; +import { IJunctionRewards } from "@/src/types/commonTypes"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -273,6 +275,20 @@ export const addMissionRewards = async ( } } + if (rewardInfo.node in junctionRewards) { + const junctionReward = (junctionRewards as IJunctionRewards)[rewardInfo.node]; + for (const item of junctionReward.items) { + MissionRewards.push({ + StoreItem: item.ItemType, + ItemCount: item.ItemCount + }); + } + if (junctionReward.credits) { + inventory.RegularCredits += junctionReward.credits; + missionCompletionCredits += junctionReward.credits; + } + } + for (const reward of MissionRewards) { //TODO: additem should take in storeItems const inventoryChange = await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount); diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts index eebd9410..5ac1cac3 100644 --- a/src/types/commonTypes.ts +++ b/src/types/commonTypes.ts @@ -1,3 +1,5 @@ +import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; + export interface IOid { $oid: string; } @@ -7,3 +9,10 @@ export interface IMongoDate { $numberLong: string; }; } + +export interface IReward { + items: ITypeCount[]; + credits: number; +} + +export type IJunctionRewards = Record; diff --git a/static/fixed_responses/junctionRewards.json b/static/fixed_responses/junctionRewards.json new file mode 100644 index 00000000..e09c56a6 --- /dev/null +++ b/static/fixed_responses/junctionRewards.json @@ -0,0 +1,120 @@ +{ + "VenusToMercuryJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedIntroQuest/InfestedIntroQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Keys/KubrowQuest/KubrowQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Rifle/BoltoRifle", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarShieldRechargeRateMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarAbilityEfficiencyMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/EggHatcher", "ItemCount": 1 } + ], + "credits": 10000 + }, + "EarthToVenusJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/FurisBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponFreezeDamageMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponElectricityDamageMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/SentinelRecipes/TnSentinelCrossBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/StaffCmbOneMeleeTree", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerEnergyHealthRegenAuraMod", "ItemCount": 1 } + ], + "credits": 5000 + }, + "EarthToMarsJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/EggHatcher", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/VoidProjectionFeatureItem", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionRevenantPrimeABronze", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/HammerWeapon", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/IronPhoenixMeleeTree", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain", "ItemCount": 1 } + ], + "credits": 15000 + }, + "MarsToCeresJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnSniperRifleBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponToxinDamageMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/DualSwordCmbOneMeleeTree", "ItemCount": 1 } + ], + "credits": 20000 + }, + "MarsToPhobosJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/SpyQuestKeyChain/SpyQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnHeavyPistolBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/Consumables/CipherBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponReloadSpeedMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarLootRadarMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst", "ItemCount": 1 } + ], + "credits": 20000 + }, + "JupiterToEuropaJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/LimboQuest/LimboQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Keys/DragonQuest/DragonQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/CorpusMinigunBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", "ItemCount": 1 } + ], + "credits": 40000 + }, + "JupiterToSaturnJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrenadeLauncherBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Keys/ProteaQuest/ProteaQuestKeyChain", "ItemCount": 1 } + ], + "credits": 40000 + }, + "SaturnToUranusJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/CorpusWhipBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Keys/DuviriQuest/DuviriQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", "ItemCount": 1 } + ], + "credits": 60000 + }, + "UranusToNeptuneJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/ReconnasorBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint", "ItemCount": 1 } + ], + "credits": 80000 + }, + "NeptuneToPlutoJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrineerFlakCannonBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint", "ItemCount": 1 } + ], + "credits": 80000 + }, + "PlutoToSednaJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/WarWithinQuest/WarWithinQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Keys/MirageQuest/MirageQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/DualDaggerBlueprint", "ItemCount": 1 } + ], + "credits": 100000 + }, + "PlutoToErisJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedAladVQuest/InfestedAladVQuestKeyChain", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/MireSwordBlueprint", "ItemCount": 1 } + ], + "credits": 100000 + }, + "CeresToJupiterJunction": { + "items": [ + { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnStaffBlueprint", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Suit/ArchwingSuitHealthMaxMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/ArchwingRifleDamageAmountMod", "ItemCount": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Melee/ArchwingMeleeDamageMod", "ItemCount": 1 } + ], + "credits": 30000 + } +} From 9f0be223e68c9c1467215fe6016411dd8c63c681 Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:46:09 +0100 Subject: [PATCH 013/776] fix: returning givekeychainitem response --- src/controllers/api/giveKeyChainTriggeredItemsController.ts | 2 +- src/services/questService.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts index 8e391b01..bff70c85 100644 --- a/src/controllers/api/giveKeyChainTriggeredItemsController.ts +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -10,7 +10,7 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, const keyChainInfo = getJSONfromString((req.body as string).toString()); const inventory = await getInventory(accountId); - const inventoryChanges = giveKeyChainItem(inventory, keyChainInfo); + const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo); await inventory.save(); res.send(inventoryChanges); diff --git a/src/services/questService.ts b/src/services/questService.ts index e51eb70a..0446e273 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -138,6 +138,8 @@ export const giveKeyChainItem = async (inventory: TInventoryDatabaseDocument, ke // items were added: update quest stage's i (item was given) updateQuestStage(inventory, keyChainInfo, { i: true }); + return inventoryChanges; + //TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory /* some items are added or removed (not sure) to the wishlist, in that case a From df70050cfd498b8ecec96aa4b54341c5a8068f75 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Feb 2025 07:10:28 +0100 Subject: [PATCH 014/776] chore: update translations --- static/webui/translations/ru.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index b6715e94..a17ba2b4 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -119,10 +119,11 @@ dict = { cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, cheats_quests: `[UNTRANSLATED] Quests`, - cheats_quests_UnlockAll: `[UNTRANSLATED] Unlock All Quests`, - cheats_quests_CompleteAll: `[UNTRANSLATED] Complete All Quests`, - cheats_quests_CompleteAllUnlocked: `[UNTRANSLATED] Complete All Unlocked Quests`, - cheats_quests_ResetAll: `[UNTRANSLATED] Reset All Quests`, + cheats_quests_unlockAll: `[UNTRANSLATED] Unlock All Quests`, + cheats_quests_completeAll: `[UNTRANSLATED] Complete All Quests`, + cheats_quests_completeAllUnlocked: `[UNTRANSLATED] Complete All Unlocked Quests`, + cheats_quests_resetAll: `[UNTRANSLATED] Reset All Quests`, + cheats_quests_giveAll: `[UNTRANSLATED] Give All Quests`, import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля будут перезаписаны в вашем аккаунте.`, import_submit: `Submit` }; From 73f8f93b1767874295c2eeaf7b120d5a122e69ad Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:21:57 -0800 Subject: [PATCH 015/776] chore: update russian translation (#987) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/987 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index a17ba2b4..408743d8 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -46,7 +46,7 @@ dict = { navbar_deleteAccount: `Удалить аккаунт`, navbar_inventory: `Инвентарь`, navbar_mods: `Моды`, - navbar_quests: `[UNTRANSLATED] Quests`, + navbar_quests: `Квесты`, navbar_cheats: `Читы`, navbar_import: `Импорт`, inventory_addItems: `Добавить предметы`, @@ -79,7 +79,7 @@ dict = { currency_PremiumCredits: `Платина`, currency_FusionPoints: `Эндо`, currency_PrimeTokens: `Королевские Айя`, - currency_owned: `У тебя есть |COUNT|.`, + currency_owned: `У тебя |COUNT|.`, powersuit_archonShardsLabel: `Ячейки осколков архонта`, powersuit_archonShardsDescription: `Вы можете использовать эти неограниченные ячейки для установки множества улучшений.`, mods_addRiven: `Добавить Мод Разлома`, @@ -118,12 +118,12 @@ dict = { cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`, cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, - cheats_quests: `[UNTRANSLATED] Quests`, - cheats_quests_unlockAll: `[UNTRANSLATED] Unlock All Quests`, - cheats_quests_completeAll: `[UNTRANSLATED] Complete All Quests`, - cheats_quests_completeAllUnlocked: `[UNTRANSLATED] Complete All Unlocked Quests`, - cheats_quests_resetAll: `[UNTRANSLATED] Reset All Quests`, - cheats_quests_giveAll: `[UNTRANSLATED] Give All Quests`, + cheats_quests: `Квесты`, + cheats_quests_unlockAll: `Разблокировать все квесты`, + cheats_quests_completeAll: `Завершить все квесты`, + cheats_quests_completeAllUnlocked: `Завершить все разблокированые квесты`, + cheats_quests_resetAll: `Сбросить прогресс всех квестов`, + cheats_quests_giveAll: `Выдать все квесты`, import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля будут перезаписаны в вашем аккаунте.`, - import_submit: `Submit` + import_submit: `Отправить` }; From a3873a1710c19af2781bd3907564334d910678f9 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:22:41 -0800 Subject: [PATCH 016/776] fix(webui): show names for zaw parts (#988) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/988 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 38add971..57792dd4 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -65,7 +65,10 @@ const getItemListsController: RequestHandler = (req, response) => { uniqueName.split("/")[5] == "SUModularSecondarySet1" || uniqueName.split("/")[5] == "SUModularPrimarySet1" || uniqueName.split("/")[5] == "InfKitGun" || - uniqueName.split("/")[5] == "HoverboardParts" + uniqueName.split("/")[5] == "HoverboardParts" || + uniqueName.split("/")[5] == "ModularMelee01" || + uniqueName.split("/")[5] == "ModularMelee02" || + uniqueName.split("/")[5] == "ModularMeleeInfested" ) { res.ModularParts.push({ uniqueName, From 9203e0bf4d0947895093c0b0f5925ecf73181e1b Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Feb 2025 11:09:17 -0800 Subject: [PATCH 017/776] feat: infiniteHelminthMaterials cheat (#985) Closes #728 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/985 Co-authored-by: Sainan Co-committed-by: Sainan --- config.json.example | 1 + .../api/infestedFoundryController.ts | 72 ++++++++++++++----- src/controllers/api/inventoryController.ts | 6 +- src/controllers/api/upgradesController.ts | 7 +- src/services/configService.ts | 1 + static/webui/index.html | 4 ++ static/webui/translations/en.js | 1 + static/webui/translations/ru.js | 1 + 8 files changed, 73 insertions(+), 20 deletions(-) diff --git a/config.json.example b/config.json.example index ec776374..1bd3b1c1 100644 --- a/config.json.example +++ b/config.json.example @@ -20,6 +20,7 @@ "infinitePlatinum": true, "infiniteEndo": true, "infiniteRegalAya": true, + "infiniteHelminthMaterials": false, "unlockAllShipFeatures": true, "unlockAllShipDecorations": true, "unlockAllFlavourItems": true, diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index 5e27f1c9..f79587b5 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -6,7 +6,9 @@ import { IOid } from "@/src/types/commonTypes"; import { IConsumedSuit, IHelminthFoodRecord, + IInfestedFoundryClient, IInfestedFoundryDatabase, + IInventoryClient, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -16,6 +18,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; import { colorToShard } from "@/src/helpers/shardHelper"; +import { config } from "@/src/services/configService"; export const infestedFoundryController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -69,18 +72,22 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { // remove from suit suit.ArchonCrystalUpgrades![request.Slot] = {}; - // remove bile - const bile = inventory.InfestedFoundry!.Resources!.find( - x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile" - )!; - bile.Count -= 300; + if (!config.infiniteHelminthMaterials) { + // remove bile + const bile = inventory.InfestedFoundry!.Resources!.find( + x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile" + )!; + bile.Count -= 300; + } await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { MiscItems: miscItemChanges, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -105,6 +112,12 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "c": { // consume items + + if (config.infiniteHelminthMaterials) { + res.status(400).end(); + return; + } + const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); inventory.InfestedFoundry ??= {}; @@ -210,9 +223,11 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { inventory.InfestedFoundry.InvigorationsApplied = 0; } await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -223,10 +238,12 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); const recipe = getRecipe(request.Recipe)!; - for (const ingredient of recipe.secretIngredients!) { - const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType); - if (resource) { - resource.Count -= ingredient.ItemCount; + if (!config.infiniteHelminthMaterials) { + for (const ingredient of recipe.secretIngredients!) { + const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType); + if (resource) { + resource.Count -= ingredient.ItemCount; + } } } const suit = inventory.Suits.id(request.SuitId.$oid)!; @@ -247,6 +264,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00); addRecipes(inventory, recipeChanges); await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { Recipes: recipeChanges, @@ -260,7 +279,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { platinum: 0, Slots: 1 }, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -272,11 +291,13 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const currencyChanges = updateCurrency(inventory, 50, true); const recipeChanges = handleSubsumeCompletion(inventory); await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { ...currencyChanges, Recipes: recipeChanges, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -292,13 +313,17 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { suit.UpgradesExpiry = upgradesExpiry; const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00); addRecipes(inventory, recipeChanges); - for (let i = 0; i != request.ResourceTypes.length; ++i) { - inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -= - request.ResourceCosts[i]; + if (!config.infiniteHelminthMaterials) { + for (let i = 0; i != request.ResourceTypes.length; ++i) { + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -= + request.ResourceCosts[i]; + } } inventory.InfestedFoundry!.InvigorationsApplied ??= 0; inventory.InfestedFoundry!.InvigorationsApplied += 1; await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ SuitId: request.SuitId, OffensiveUpgrade: request.OffensiveUpgradeType, @@ -306,7 +331,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { UpgradesExpiry: toMongoDate(upgradesExpiry), InventoryChanges: { Recipes: recipeChanges, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -453,6 +478,19 @@ export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): return recipeChanges; }; +export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => { + if (config.infiniteHelminthMaterials) { + infestedFoundry.Resources = [ + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 } + ]; + } +}; + interface IHelminthOfferingsUpdate { OfferingsIndex: number; SuitTypes: string[]; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index df7cebcf..82b55ff5 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -13,7 +13,7 @@ import { ExportResources, ExportVirtuals } from "warframe-public-export-plus"; -import { handleSubsumeCompletion } from "./infestedFoundryController"; +import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; export const inventoryController: RequestHandler = async (request, response) => { @@ -212,6 +212,10 @@ export const getInventoryResponse = async ( } } + if (inventoryResponse.InfestedFoundry) { + applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); + } + // Fix for #380 inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } }; diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index d295a462..c29e20fa 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -12,6 +12,7 @@ import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/se import { getRecipeByResult } from "@/src/services/itemDataService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { addInfestedFoundryXP } from "./infestedFoundryController"; +import { config } from "@/src/services/configService"; export const upgradesController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -48,8 +49,10 @@ export const upgradesController: RequestHandler = async (req, res) => { const recipe = getRecipeByResult(operation.UpgradeRequirement)!; for (const ingredient of recipe.ingredients) { totalPercentagePointsConsumed += ingredient.ItemCount / 10; - inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -= - ingredient.ItemCount; + if (!config.infiniteHelminthMaterials) { + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -= + ingredient.ItemCount; + } } } diff --git a/src/services/configService.ts b/src/services/configService.ts index bb4d5c96..e1b9c984 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -46,6 +46,7 @@ interface IConfig { infinitePlatinum?: boolean; infiniteEndo?: boolean; infiniteRegalAya?: boolean; + infiniteHelminthMaterials?: boolean; unlockAllShipFeatures?: boolean; unlockAllShipDecorations?: boolean; unlockAllFlavourItems?: boolean; diff --git a/static/webui/index.html b/static/webui/index.html index 081ca3d2..0d4b3e95 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -473,6 +473,10 @@ +
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 77cadccc..3251cd63 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -99,6 +99,7 @@ dict = { cheats_infinitePlatinum: `Infinite Platinum`, cheats_infiniteEndo: `Infinite Endo`, cheats_infiniteRegalAya: `Infinite Regal Aya`, + cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`, cheats_unlockAllShipFeatures: `Unlock All Ship Features`, cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`, cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 408743d8..781046b9 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -100,6 +100,7 @@ dict = { cheats_infinitePlatinum: `Бесконечная платина`, cheats_infiniteEndo: `Бесконечное эндо`, cheats_infiniteRegalAya: `Бесконечная Королевская Айя`, + cheats_infiniteHelminthMaterials: `[UNTRANSLATED] Infinite Helminth Materials`, cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`, cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`, cheats_unlockAllFlavourItems: `Разблокировать все уникальные предметы`, From bf7fd42198c2ce535c035ae679312aec3d5a4722 Mon Sep 17 00:00:00 2001 From: Ordis <134585663+OrdisPrime@users.noreply.github.com> Date: Sat, 22 Feb 2025 11:10:52 -0800 Subject: [PATCH 018/776] feat: tutorial and natural new player experience (#983) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/983 Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com> --- package-lock.json | 6 +- .../api/getNewRewardSeedController.ts | 6 +- .../api/giveStartingGearController.ts | 96 ++++++++++++++ .../custom/manageQuestsController.ts | 3 - src/models/inventoryModels/inventoryModel.ts | 73 +++++++++-- src/routes/api.ts | 2 + src/services/inboxService.ts | 4 +- src/services/inventoryService.ts | 121 ++++++++---------- src/services/questService.ts | 3 +- src/types/inventoryTypes/inventoryTypes.ts | 26 +++- static/fixed_responses/eventMessages.json | 12 ++ static/fixed_responses/messages.json | 49 ------- 12 files changed, 257 insertions(+), 144 deletions(-) create mode 100644 src/controllers/api/giveStartingGearController.ts create mode 100644 static/fixed_responses/eventMessages.json delete mode 100644 static/fixed_responses/messages.json diff --git a/package-lock.json b/package-lock.json index ec52be41..a43d45a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4093,9 +4093,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.30", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.30.tgz", - "integrity": "sha512-vzs+naEqp3iFZTbgIky4jiNbjNIovuR4oSimrFiuyIbrnfTlfXFzDfzT0hG2rgS8yEXBAbOcv2Zfm3fmWuZ0Kg==" + "version": "0.5.32", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.32.tgz", + "integrity": "sha512-jO9i2Gzz9DWibiHlEO17D975ajs6KrTay8cS5I0GkUUe1XWVU8mML4b+IYCHzM4FWq1t6p2YPCGznQfknqvorg==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index 163e3c6e..bbb3f71b 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,13 +1,11 @@ import { RequestHandler } from "express"; -const getNewRewardSeedController: RequestHandler = (_req, res) => { +export const getNewRewardSeedController: RequestHandler = (_req, res) => { res.json({ rewardSeed: generateRewardSeed() }); }; -function generateRewardSeed(): number { +export function generateRewardSeed(): number { const min = -Number.MAX_SAFE_INTEGER; const max = Number.MAX_SAFE_INTEGER; return Math.floor(Math.random() * (max - min + 1)) + min; } - -export { getNewRewardSeedController }; diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts new file mode 100644 index 00000000..ef9be78f --- /dev/null +++ b/src/controllers/api/giveStartingGearController.ts @@ -0,0 +1,96 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel"; +import { + addEquipment, + addItem, + combineInventoryChanges, + getInventory, + updateSlots +} from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { HydratedDocument } from "mongoose"; + +type TPartialStartingGear = Pick; + +export const giveStartingGearController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const startingGear = getJSONfromString(String(req.body)); + const inventory = await getInventory(accountId); + + const inventoryChanges = await addStartingGear(inventory, startingGear); + await inventory.save(); + + res.send(inventoryChanges); +}; + +//TODO: RawUpgrades might need to return a LastAdded +const awakeningRewards = [ + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4", + "/Lotus/Types/Restoratives/LisetAutoHack", + "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" +]; + +export const addStartingGear = async ( + inventory: HydratedDocument, + startingGear: TPartialStartingGear | undefined = undefined +) => { + const { LongGuns, Pistols, Suits, Melee } = startingGear || { + LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], + Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], + Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }], + Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }] + }; + + //TODO: properly merge weapon bin changes it is currently static here + const inventoryChanges: IInventoryChanges = {}; + addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Suits", Suits[0].ItemType, undefined, inventoryChanges, { Configs: Suits[0].Configs }); + addEquipment( + inventory, + "DataKnives", + "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", + undefined, + inventoryChanges, + { XP: 450_000 } + ); + addEquipment( + inventory, + "Scoops", + "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", + undefined, + inventoryChanges + ); + + updateSlots(inventory, InventorySlot.SUITS, 0, 1); + updateSlots(inventory, InventorySlot.WEAPONS, 0, 3); + inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 }; + inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 }; + + await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); + inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"; + + inventory.PremiumCredits = 50; + inventory.PremiumCreditsFree = 50; + inventoryChanges.PremiumCredits = 50; + inventoryChanges.PremiumCreditsFree = 50; + inventory.RegularCredits = 3000; + inventoryChanges.RegularCredits = 3000; + + for (const item of awakeningRewards) { + const inventoryDelta = await addItem(inventory, item); + combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); + } + + inventory.PlayedParkourTutorial = true; + inventory.ReceivedStartingGear = true; + + return inventoryChanges; +}; diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index edf1feae..6899b76d 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -63,8 +63,6 @@ export const manageQuestsController: RequestHandler = async (req, res) => { inventory.ArchwingEnabled = true; } } - - inventory.ActiveQuest = ""; break; } case "ResetAll": { @@ -103,7 +101,6 @@ export const manageQuestsController: RequestHandler = async (req, res) => { inventory.ArchwingEnabled = true; } } - inventory.ActiveQuest = ""; break; } case "giveAll": { diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index fe651349..3ec3c363 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -64,7 +64,11 @@ import { IKubrowPetEggClient, ICustomMarkers, IMarkerInfo, - IMarker + IMarker, + ICalendarProgress, + IPendingCouponDatabase, + IPendingCouponClient, + ILibraryAvailableDailyTaskInfo } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -323,7 +327,7 @@ MailboxSchema.set("toJSON", { const DuviriInfoSchema = new Schema( { Seed: Number, - NumCompletions: Number + NumCompletions: { type: Number, default: 0 } }, { _id: false, @@ -435,6 +439,7 @@ const seasonChallengeHistorySchema = new Schema( //TODO: check whether this is complete const playerSkillsSchema = new Schema( { + LPP_NONE: { type: Number, default: 0 }, LPP_SPACE: { type: Number, default: 0 }, LPS_PILOTING: { type: Number, default: 0 }, LPS_GUNNERY: { type: Number, default: 0 }, @@ -891,19 +896,63 @@ const CustomMarkersSchema = new Schema( { _id: false } ); +const calenderProgressSchema = new Schema( + { + Version: { type: Number, default: 19 }, + Iteration: { type: Number, default: 2 }, + YearProgress: { + Upgrades: { type: [] } + }, + SeasonProgress: { + SeasonType: String, + LastCompletedDayIdx: { type: Number, default: -1 }, + LastCompletedChallengeDayIdx: { type: Number, default: -1 }, + ActivatedChallenges: [] + } + }, + { _id: false } +); + +const pendingCouponSchema = new Schema( + { + Expiry: { type: Date, default: new Date(0) }, + Discount: { type: Number, default: 0 } + }, + { _id: false } +); + +pendingCouponSchema.set("toJSON", { + transform(_doc, ret, _options) { + (ret as IPendingCouponClient).Expiry = toMongoDate((ret as IPendingCouponDatabase).Expiry); + } +}); + +const libraryAvailableDailyTaskInfoSchema = new Schema( + { + EnemyTypes: [String], + EnemyLocTag: String, + EnemyIcon: String, + ScansRequired: Number, + RewardStoreItem: String, + RewardQuantity: Number, + RewardStanding: Number + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, - SubscribedToEmails: Number, - Created: Date, + SubscribedToEmails: { type: Number, default: 0 }, + SubscribedToEmailsPersonalized: { type: Number, default: 0 }, RewardSeed: Number, //Credit RegularCredits: { type: Number, default: 0 }, //Platinum - PremiumCredits: { type: Number, default: 50 }, + PremiumCredits: { type: Number, default: 0 }, //Gift Platinum(Non trade) - PremiumCreditsFree: { type: Number, default: 50 }, + PremiumCreditsFree: { type: Number, default: 0 }, //Endo FusionPoints: { type: Number, default: 0 }, //Regal Aya @@ -911,7 +960,7 @@ const inventorySchema = new Schema( //Slots SuitBin: { type: slotsBinSchema, default: { Slots: 3 } }, - WeaponBin: { type: slotsBinSchema, default: { Slots: 10 } }, + WeaponBin: { type: slotsBinSchema, default: { Slots: 11 } }, SentinelBin: { type: slotsBinSchema, default: { Slots: 10 } }, SpaceSuitBin: { type: slotsBinSchema, default: { Slots: 4 } }, SpaceWeaponBin: { type: slotsBinSchema, default: { Slots: 4 } }, @@ -1022,7 +1071,7 @@ const inventorySchema = new Schema( //Complete Mission\Quests Missions: [missionSchema], QuestKeys: [questKeysSchema], - ActiveQuest: { type: String, default: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" }, //TODO: check after mission starting gear + ActiveQuest: { type: String, default: "" }, //item like DojoKey or Boss missions key LevelKeys: [Schema.Types.Mixed], //Active quests @@ -1137,7 +1186,7 @@ const inventorySchema = new Schema( //Cephalon Simaris Entries Example:"TargetType"+"Scans"(1-10)+"Completed": true|false LibraryPersonalProgress: [Schema.Types.Mixed], //Cephalon Simaris Daily Task - LibraryAvailableDailyTaskInfo: Schema.Types.Mixed, + LibraryAvailableDailyTaskInfo: libraryAvailableDailyTaskInfoSchema, //https://warframe.fandom.com/wiki/Invasion InvasionChainProgress: [Schema.Types.Mixed], @@ -1184,7 +1233,6 @@ const inventorySchema = new Schema( HandlerPoints: Number, ChallengesFixVersion: Number, PlayedParkourTutorial: Boolean, - SubscribedToEmailsPersonalized: Number, ActiveLandscapeTraps: [Schema.Types.Mixed], RepVotes: [Schema.Types.Mixed], LeagueTickets: [Schema.Types.Mixed], @@ -1202,7 +1250,7 @@ const inventorySchema = new Schema( HasResetAccount: { type: Boolean, default: false }, //Discount Coupon - PendingCoupon: Schema.Types.Mixed, + PendingCoupon: pendingCouponSchema, //Like BossAladV,BossCaptainVor come for you on missions % chance DeathMarks: [String], //Zanuka @@ -1212,7 +1260,8 @@ const inventorySchema = new Schema( EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, - DialogueHistory: dialogueHistorySchema + DialogueHistory: dialogueHistorySchema, + CalendarProgress: calenderProgressSchema }, { timestamps: { createdAt: "Created", updatedAt: false } } ); diff --git a/src/routes/api.ts b/src/routes/api.ts index 7ad9e5a6..41c5f460 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -87,6 +87,7 @@ import { updateSessionGetController, updateSessionPostController } from "@/src/c import { updateThemeController } from "../controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; import { saveSettingsController } from "../controllers/api/saveSettingsController"; +import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; const apiRouter = express.Router(); @@ -146,6 +147,7 @@ apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); +apiRouter.post("/giveStartingGear.php", giveStartingGearController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); apiRouter.post("/infestedFoundry.php", infestedFoundryController); diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index 11768054..895699f1 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -2,7 +2,7 @@ import { IMessageDatabase, Inbox } from "@/src/models/inboxModel"; import { getAccountForRequest } from "@/src/services/loginService"; import { HydratedDocument } from "mongoose"; import { Request } from "express"; -import messages from "@/static/fixed_responses/messages.json"; +import eventMessages from "@/static/fixed_responses/eventMessages.json"; import { logger } from "@/src/utils/logger"; export const getAllMessagesSorted = async (accountId: string): Promise[]> => { @@ -32,7 +32,7 @@ export const createNewEventMessages = async (req: Request) => { const latestEventMessageDate = account.LatestEventMessageDate; //TODO: is baroo there? create these kind of messages too (periodical messages) - const newEventMessages = messages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate); + const newEventMessages = eventMessages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate); if (newEventMessages.length === 0) { logger.debug(`No new event messages. Latest event message date: ${latestEventMessageDate.toISOString()}`); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index c43e1d1c..2ab822e6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -22,7 +22,9 @@ import { IDailyAffiliations, IInventoryDatabase, IKubrowPetEggDatabase, - IKubrowPetEggClient + IKubrowPetEggClient, + ILibraryAvailableDailyTaskInfo, + ICalendarProgress } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate } from "../types/genericUpdate"; import { @@ -32,7 +34,7 @@ import { } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { getExalted, getKeyChainItems } from "@/src/services/itemDataService"; -import { IEquipmentClient, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; +import { IEquipmentClient, IEquipmentDatabase, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; import { ExportArcanes, ExportCustoms, @@ -51,6 +53,9 @@ import { createShip } from "./shipService"; import { creditBundles, fusionBundles } from "@/src/services/missionInventoryUpdateService"; import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { toOid } from "../helpers/inventoryHelpers"; +import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; +import { addStartingGear } from "@/src/controllers/api/giveStartingGearController"; +import { completeQuest } from "@/src/services/questService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -65,65 +70,18 @@ export const createInventory = async ( ReceivedStartingGear: config.skipTutorial }); + inventory.LibraryAvailableDailyTaskInfo = createLibraryAvailableDailyTaskInfo(); + inventory.CalendarProgress = createCalendar(); + inventory.RewardSeed = generateRewardSeed(); + inventory.DuviriInfo = { + Seed: generateRewardSeed(), + NumCompletions: 0 + }; + await addItem(inventory, "/Lotus/Types/Friendly/PlayerControllable/Weapons/DuviriDualSwords"); + if (config.skipTutorial) { - const defaultEquipment = [ - // Awakening rewards - { ItemCount: 1, ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }, - { ItemCount: 1, ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }, - { ItemCount: 1, ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }, - { ItemCount: 1, ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4" }, - { ItemCount: 1, ItemType: "/Lotus/Types/Restoratives/LisetAutoHack" } - ]; - - // const vorsPrizeRewards = [ - // // Vor's Prize rewards - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarHealthMaxMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityRangeMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityStrengthMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityDurationMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPickupBonusMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPowerMaxMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarEnemyRadarMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponFireRateMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponMeleeDamageMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageCorpus" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageGrineer" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponDamageAmountMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponFireDamageMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponElectricityDamageMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponDamageAmountMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Types/Recipes/Weapons/BurstonRifleBlueprint" }, - // { ItemCount: 1, ItemType: "/Lotus/Types/Items/MiscItems/Morphic" }, - // { ItemCount: 400, ItemType: "/Lotus/Types/Items/MiscItems/PolymerBundle" }, - // { ItemCount: 150, ItemType: "/Lotus/Types/Items/MiscItems/AlloyPlate" } - // ]; - for (const equipment of defaultEquipment) { - await addItem(inventory, equipment.ItemType, equipment.ItemCount); - } - - // Missing in Public Export - inventory.Horses.push({ - ItemType: "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit" - }); - inventory.DataKnives.push({ - ItemType: "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", - XP: 450000 - }); - inventory.Scoops.push({ - ItemType: "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest" - }); - inventory.DrifterMelee.push({ - ItemType: "/Lotus/Types/Friendly/PlayerControllable/Weapons/DuviriDualSwords" - }); - - inventory.QuestKeys.push({ - ItemType: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" - }); + await addStartingGear(inventory); + await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); const completedMissions = ["SolNode27", "SolNode89", "SolNode63", "SolNode85", "SolNode15", "SolNode79"]; @@ -133,14 +91,11 @@ export const createInventory = async ( Tag: tag })) ); - - inventory.RegularCredits = 25000; - inventory.FusionPoints = 180; } await inventory.save(); } catch (error) { - throw new Error(`Error creating inventory: ${error instanceof Error ? error.message : "Unknown error"}`); + throw new Error(`Error creating inventory: ${error instanceof Error ? error.message : "Unknown error type"}`); } }; @@ -150,6 +105,7 @@ export const createInventory = async ( * @param InventoryChanges - will hold the combined changes * @param delta - inventory changes to be added */ +//TODO: this fails silently when providing an incorrect object to delta export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => { for (const key in delta) { if (!(key in InventoryChanges)) { @@ -779,15 +735,19 @@ export const addEquipment = ( category: TEquipmentKey, type: string, modularParts: string[] | undefined = undefined, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + defaultOverwrites: Partial | undefined = undefined ): IInventoryChanges => { - const index = - inventory[category].push({ + const equipment = Object.assign( + { ItemType: type, Configs: [], XP: 0, ModularParts: modularParts - }) - 1; + }, + defaultOverwrites + ); + const index = inventory[category].push(equipment) - 1; inventoryChanges[category] ??= []; inventoryChanges[category].push(inventory[category][index].toJSON()); @@ -1173,3 +1133,28 @@ export const addKeyChainItems = async ( return inventoryChanges; }; +const createLibraryAvailableDailyTaskInfo = (): ILibraryAvailableDailyTaskInfo => { + return { + EnemyTypes: ["/Lotus/Types/Enemies/Orokin/RifleLancerAvatar"], + EnemyLocTag: "/Lotus/Language/Game/CorruptedLancer", + EnemyIcon: "/Lotus/Interface/Icons/Npcs/OrokinRifleLancerAvatar.png", + ScansRequired: 3, + RewardStoreItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/UncommonFusionBundle", + RewardQuantity: 7, + RewardStanding: 7500 + }; +}; + +const createCalendar = (): ICalendarProgress => { + return { + Version: 19, + Iteration: 2, + YearProgress: { Upgrades: [] }, + SeasonProgress: { + SeasonType: "CST_SPRING", + LastCompletedDayIdx: -1, + LastCompletedChallengeDayIdx: -1, + ActivatedChallenges: [] + } + }; +}; diff --git a/src/services/questService.ts b/src/services/questService.ts index 0446e273..630ca60a 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -126,7 +126,8 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest }); } } - //TODO: handle quest completions + inventory.ActiveQuest = ""; + //TODO: handle quest completion items }; export const giveKeyChainItem = async (inventory: TInventoryDatabaseDocument, keyChainInfo: IKeyChainRequest) => { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index fab41e4e..e9375863 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -38,6 +38,7 @@ export interface IInventoryDatabase | "InfestedFoundry" | "DialogueHistory" | "KubrowPetEggs" + | "PendingCoupon" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -61,6 +62,7 @@ export interface IInventoryDatabase InfestedFoundry?: IInfestedFoundryDatabase; DialogueHistory?: IDialogueHistoryDatabase; KubrowPetEggs?: IKubrowPetEggDatabase[]; + PendingCoupon: IPendingCouponDatabase; } export interface IQuestKeyDatabase { @@ -318,11 +320,12 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CollectibleSeries: ICollectibleSery[]; LibraryAvailableDailyTaskInfo: ILibraryAvailableDailyTaskInfo; HasResetAccount: boolean; - PendingCoupon: IPendingCoupon; + PendingCoupon: IPendingCouponClient; Harvestable: boolean; DeathSquadable: boolean; EndlessXP?: IEndlessXpProgress[]; DialogueHistory?: IDialogueHistoryClient; + CalendarProgress: ICalendarProgress; } export interface IAffiliation { @@ -759,7 +762,12 @@ export enum Manifest { LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo" } -export interface IPendingCoupon { +export interface IPendingCouponDatabase { + Expiry: Date; + Discount: number; +} + +export interface IPendingCouponClient { Expiry: IMongoDate; Discount: number; } @@ -858,6 +866,7 @@ export interface IPersonalTechProject { } export interface IPlayerSkills { + LPP_NONE: number; LPP_SPACE: number; LPS_PILOTING: number; LPS_GUNNERY: number; @@ -1042,3 +1051,16 @@ export interface IMarker { z: number; showInHud: boolean; } +export interface ISeasonProgress { + SeasonType: "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"; + LastCompletedDayIdx: number; + LastCompletedChallengeDayIdx: number; + ActivatedChallenges: unknown[]; +} + +export interface ICalendarProgress { + Version: number; + Iteration: number; + YearProgress: { Upgrades: unknown[] }; + SeasonProgress: ISeasonProgress; +} diff --git a/static/fixed_responses/eventMessages.json b/static/fixed_responses/eventMessages.json new file mode 100644 index 00000000..6ecf6d44 --- /dev/null +++ b/static/fixed_responses/eventMessages.json @@ -0,0 +1,12 @@ +{ + "Messages": [ + { + "sub": "Welcome to Space Ninja Server", + "sndr": "/Lotus/Language/Bosses/Ordis", + "msg": "Enjoy your Space Ninja Experience", + "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", + "eventMessageDate": "2025-01-30T13:00:00.000Z", + "r": false + } + ] +} diff --git a/static/fixed_responses/messages.json b/static/fixed_responses/messages.json deleted file mode 100644 index a67069c7..00000000 --- a/static/fixed_responses/messages.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "Messages": [ - { - "sub": "Welcome to Space Ninja Server", - "sndr": "/Lotus/Language/Bosses/Ordis", - "msg": "Enjoy your Space Ninja Experience", - "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", - "eventMessageDate": "2025-01-30T13:00:00.000Z", - "r": false - }, - { - "sub": "/Lotus/Language/Inbox/DarvoWeaponCraftingMessageBTitle", - "sndr": "/Lotus/Language/Bosses/Darvo", - "msg": "/Lotus/Language/Inbox/DarvoWeaponCraftingMessageBDesc", - "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", - "countedAtt": [ - { - "ItemCount": 1, - "ItemType": "/Lotus/Types/Recipes/Weapons/BurstonRifleBlueprint" - }, - { - "ItemCount": 1, - "ItemType": "/Lotus/Types/Items/MiscItems/Morphic" - }, - { - "ItemCount": 400, - "ItemType": "/Lotus/Types/Items/MiscItems/PolymerBundle" - }, - { - "ItemCount": 150, - "ItemType": "/Lotus/Types/Items/MiscItems/AlloyPlate" - } - ], - "highPriority": true, - "eventMessageDate": "2023-10-01T17:00:00.000Z", - "r": false - }, - { - "sub": "/Lotus/Language/G1Quests/Beginner_Growth_Inbox_Title", - "sndr": "/Lotus/Language/Menu/Mailbox_WarframeSender", - "msg": "/Lotus/Language/G1Quests/Beginner_Growth_Inbox_Desc", - "icon": "/Lotus/Interface/Icons/Npcs/Lotus_d.png", - "transmission": "/Lotus/Sounds/Dialog/VorsPrize/DLisetPostAssassinate110Lotus", - "highPriority": true, - "eventMessageDate": "2023-09-01T17:00:00.000Z", - "r": false - } - ] -} From 02c0c1c2f2b2db5a60d7be4c35a719e81af6c444 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Feb 2025 04:33:28 +0100 Subject: [PATCH 019/776] chore: fix imports in api.ts --- src/routes/api.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/routes/api.ts b/src/routes/api.ts index 41c5f460..f4b0ea08 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -3,8 +3,8 @@ import { activateRandomModController } from "@/src/controllers/api/activateRando import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; -import { artifactsController } from "../controllers/api/artifactsController"; -import { changeDojoRootController } from "../controllers/api/changeDojoRootController"; +import { artifactsController } from "@/src/controllers/api/artifactsController"; +import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; @@ -25,7 +25,7 @@ import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDail import { getFriendsController } from "@/src/controllers/api/getFriendsController"; import { getGuildController } from "@/src/controllers/api/getGuildController"; import { getGuildDojoController } from "@/src/controllers/api/getGuildDojoController"; -import { getGuildLogController } from "../controllers/api/getGuildLogController"; +import { getGuildLogController } from "@/src/controllers/api/getGuildLogController"; import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController"; import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController"; import { getShipController } from "@/src/controllers/api/getShipController"; @@ -35,7 +35,8 @@ import { gildWeaponController } from "@/src/controllers/api/gildWeaponController import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey"; -import { guildTechController } from "../controllers/api/guildTechController"; +import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; +import { guildTechController } from "@/src/controllers/api/guildTechController"; import { hostSessionController } from "@/src/controllers/api/hostSessionController"; import { hubController } from "@/src/controllers/api/hubController"; import { hubInstancesController } from "@/src/controllers/api/hubInstancesController"; @@ -53,12 +54,13 @@ import { modularWeaponCraftingController } from "@/src/controllers/api/modularWe import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; -import { projectionManagerController } from "../controllers/api/projectionManagerController"; +import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadout"; +import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; import { sellController } from "@/src/controllers/api/sellController"; import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController"; import { setActiveShipController } from "@/src/controllers/api/setActiveShipController"; @@ -69,25 +71,23 @@ import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDeco import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController"; -import { setWeaponSkillTreeController } from "../controllers/api/setWeaponSkillTreeController"; +import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController"; import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController"; import { startDojoRecipeController } from "@/src/controllers/api/startDojoRecipeController"; import { startLibraryPersonalTargetController } from "@/src/controllers/api/startLibraryPersonalTargetController"; import { startRecipeController } from "@/src/controllers/api/startRecipeController"; import { stepSequencersController } from "@/src/controllers/api/stepSequencersController"; import { surveysController } from "@/src/controllers/api/surveysController"; -import { syndicateSacrificeController } from "../controllers/api/syndicateSacrificeController"; -import { syndicateStandingBonusController } from "../controllers/api/syndicateStandingBonusController"; +import { syndicateSacrificeController } from "@/src/controllers/api/syndicateSacrificeController"; +import { syndicateStandingBonusController } from "@/src/controllers/api/syndicateStandingBonusController"; import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController"; import { trainingResultController } from "@/src/controllers/api/trainingResultController"; import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController"; import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController"; import { updateQuestController } from "@/src/controllers/api/updateQuestController"; import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController"; -import { updateThemeController } from "../controllers/api/updateThemeController"; +import { updateThemeController } from "@/src/controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; -import { saveSettingsController } from "../controllers/api/saveSettingsController"; -import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; const apiRouter = express.Router(); From 0142aa72a8532674ea12b4b64174f0cb2eed4191 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Feb 2025 05:00:41 +0100 Subject: [PATCH 020/776] chore: sort api post routes --- src/routes/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/api.ts b/src/routes/api.ts index f4b0ea08..2b3e511c 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -163,6 +163,7 @@ apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveLoadout.php", saveLoadoutController); +apiRouter.post("/saveSettings.php", saveSettingsController); apiRouter.post("/sell.php", sellController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); @@ -185,6 +186,5 @@ apiRouter.post("/updateQuest.php", updateQuestController); apiRouter.post("/updateSession.php", updateSessionPostController); apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/upgrades.php", upgradesController); -apiRouter.post("/saveSettings.php", saveSettingsController); export { apiRouter }; From e1af6bd598feeeffb8234319ca5ed37ba7213abb Mon Sep 17 00:00:00 2001 From: nrbdev Date: Sun, 23 Feb 2025 03:53:56 -0800 Subject: [PATCH 021/776] feat: implement CreditBundle purchases (#989) This fixes purchasing one of the few bundles that include these credit bundles. Ex: Essential Damage mod bundles Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/989 Co-authored-by: nrbdev Co-committed-by: nrbdev --- src/services/missionInventoryUpdateService.ts | 4 +++- src/services/purchaseService.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 766518e2..d579662f 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -62,7 +62,9 @@ export const creditBundles: Record = { "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000, "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000, "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000, - "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000 + "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000, + "/Lotus/Types/StoreItems/CreditBundles/CreditBundleA": 50000, + "/Lotus/Types/StoreItems/CreditBundles/CreditBundleC": 175000 }; export const fusionBundles: Record = { diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index bc2ef924..7b59ab52 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -25,6 +25,7 @@ import { } from "warframe-public-export-plus"; import { config } from "./configService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { creditBundles } from "./missionInventoryUpdateService"; export const getStoreItemCategory = (storeItem: string): string => { const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); @@ -330,6 +331,22 @@ const handleBoosterPackPurchase = async ( return purchaseResponse; }; +const handleCreditBundlePurchase = async ( + typeName: string, + inventory: TInventoryDatabaseDocument +): Promise => { + if (typeName && typeName in creditBundles) { + const creditsAmount = creditBundles[typeName]; + + inventory.RegularCredits += creditsAmount; + await inventory.save(); + + return { InventoryChanges: { RegularCredits: creditsAmount } }; + } else { + throw new Error(`unknown credit bundle: ${typeName}`); + } +}; + //TODO: change to getInventory, apply changes then save at the end const handleTypesPurchase = async ( typesName: string, @@ -345,6 +362,8 @@ const handleTypesPurchase = async ( return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": return handleSlotPurchase(typesName, inventory, quantity); + case "CreditBundles": + return handleCreditBundlePurchase(typesName, inventory); } }; From 3c2d194302e0f49ea5e622f8d2a9f35dd7d7dd52 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Feb 2025 03:54:26 -0800 Subject: [PATCH 022/776] chore: replace fusionBundles map with ExportFusionBundles (#994) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/994 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 7 ++++--- src/services/missionInventoryUpdateService.ts | 11 +++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2ab822e6..1cc12c5b 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -39,6 +39,7 @@ import { ExportArcanes, ExportCustoms, ExportFlavour, + ExportFusionBundles, ExportGear, ExportKeys, ExportRecipes, @@ -50,7 +51,7 @@ import { TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; -import { creditBundles, fusionBundles } from "@/src/services/missionInventoryUpdateService"; +import { creditBundles } from "@/src/services/missionInventoryUpdateService"; import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { toOid } from "../helpers/inventoryHelpers"; import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; @@ -313,8 +314,8 @@ export const addItem = async ( } }; } - if (typeName in fusionBundles) { - const fusionPointsTotal = fusionBundles[typeName] * quantity; + if (typeName in ExportFusionBundles) { + const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity; inventory.FusionPoints += fusionPointsTotal; return { InventoryChanges: { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index d579662f..3d10bc39 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1,4 +1,4 @@ -import { ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus"; +import { ExportFusionBundles, ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, getRandomReward } from "@/src/services/rngService"; @@ -67,12 +67,6 @@ export const creditBundles: Record = { "/Lotus/Types/StoreItems/CreditBundles/CreditBundleC": 175000 }; -export const fusionBundles: Record = { - "/Lotus/Upgrades/Mods/FusionBundles/CommonFusionBundle": 15, - "/Lotus/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50, - "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle": 80 -}; - //type TMissionInventoryUpdateKeys = keyof IMissionInventoryUpdateRequest; //const ignoredInventoryUpdateKeys = ["FpsAvg", "FpsMax", "FpsMin", "FpsSamples"] satisfies TMissionInventoryUpdateKeys[]; // for keys with no meaning for this server //type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number]; @@ -165,7 +159,8 @@ export const addMissionInventoryUpdates = ( case "FusionBundles": { let fusionPoints = 0; for (const fusionBundle of value) { - const fusionPointsTotal = fusionBundles[fusionBundle.ItemType] * fusionBundle.ItemCount; + const fusionPointsTotal = + ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount; inventory.FusionPoints += fusionPointsTotal; fusionPoints += fusionPointsTotal; } From 84d7b5a62e36ca3a9c6fb2f907d6e66c72659024 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Feb 2025 03:54:46 -0800 Subject: [PATCH 023/776] fix: handle droptable giving a 3-day booster (#993) e.g. sorties can rarely give these Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/993 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 3d10bc39..5d7ecf16 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -11,7 +11,6 @@ import { addFocusXpIncreases, addFusionTreasures, addGearExpByCategory, - addItem, addMiscItems, addMissionComplete, addMods, @@ -28,6 +27,7 @@ import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import junctionRewards from "@/static/fixed_responses/junctionRewards.json"; import { IJunctionRewards } from "@/src/types/commonTypes"; +import { handleStoreItemAcquisition } from "./purchaseService"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -287,8 +287,7 @@ export const addMissionRewards = async ( } for (const reward of MissionRewards) { - //TODO: additem should take in storeItems - const inventoryChange = await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount); + const inventoryChange = await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount); //TODO: combineInventoryChanges improve type safety, merging 2 of the same item? //TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them, but the client also merges them //TODO: some conditional types to rule out binchanges? From 837e041db871dba87b7398aa7af8a18ffe442c3c Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Feb 2025 03:55:15 -0800 Subject: [PATCH 024/776] feat: unveil riven with cipher (#992) Related to #722 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/992 Co-authored-by: Sainan Co-committed-by: Sainan --- .../completeRandomModChallengeController.ts | 56 +++++++++++++++++++ .../api/rerollRandomModController.ts | 42 +------------- src/helpers/rivenFingerprintHelper.ts | 38 +++++++++++++ src/routes/api.ts | 2 + 4 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 src/controllers/api/completeRandomModChallengeController.ts create mode 100644 src/helpers/rivenFingerprintHelper.ts diff --git a/src/controllers/api/completeRandomModChallengeController.ts b/src/controllers/api/completeRandomModChallengeController.ts new file mode 100644 index 00000000..c2578eb7 --- /dev/null +++ b/src/controllers/api/completeRandomModChallengeController.ts @@ -0,0 +1,56 @@ +import { RequestHandler } from "express"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; +import { getRandomElement, getRandomInt } from "@/src/services/rngService"; +import { ExportUpgrades } from "warframe-public-export-plus"; + +export const completeRandomModChallengeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const request = getJSONfromString(String(req.body)); + let inventoryChanges: IInventoryChanges = {}; + + // Remove 20 plat or riven cipher + if ((req.query.p as string) == "1") { + inventoryChanges = { ...updateCurrency(inventory, 20, true) }; + } else { + const miscItemChanges: IMiscItem[] = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/RivenIdentifier", + ItemCount: -1 + } + ]; + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + // Update riven fingerprint to a randomised unveiled state + const upgrade = inventory.Upgrades.id(request.ItemId)!; + const meta = ExportUpgrades[upgrade.ItemType]; + const fingerprint: IUnveiledRivenFingerprint = { + compat: getRandomElement(meta.compatibleItems!), + lim: 0, + lvl: 0, + lvlReq: getRandomInt(8, 16), + pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), + buffs: [], + curses: [] + }; + randomiseRivenStats(meta, fingerprint); + upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + + await inventory.save(); + + res.json({ + InventoryChanges: inventoryChanges, + Fingerprint: upgrade.UpgradeFingerprint + }); +}; + +interface ICompleteRandomModChallengeRequest { + ItemId: string; +} diff --git a/src/controllers/api/rerollRandomModController.ts b/src/controllers/api/rerollRandomModController.ts index 71bc8f1c..e35df1a8 100644 --- a/src/controllers/api/rerollRandomModController.ts +++ b/src/controllers/api/rerollRandomModController.ts @@ -2,8 +2,8 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; -import { getRandomElement } from "@/src/services/rngService"; export const rerollRandomModController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -25,7 +25,7 @@ export const rerollRandomModController: RequestHandler = async (req, res) => { fingerprint.rerolls++; upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); - randomiseStats(upgrade.ItemType, fingerprint); + randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); await inventory.save(); @@ -52,28 +52,6 @@ export const rerollRandomModController: RequestHandler = async (req, res) => { } }; -const randomiseStats = (randomModType: string, fingerprint: IUnveiledRivenFingerprint): void => { - const meta = ExportUpgrades[randomModType]; - - fingerprint.buffs = []; - const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 - const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff); - for (let i = 0; i != numBuffs; ++i) { - const buffIndex = Math.trunc(Math.random() * buffEntries.length); - const entry = buffEntries[buffIndex]; - fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - buffEntries.splice(buffIndex, 1); - } - - fingerprint.curses = []; - if (Math.random() < 0.5) { - const entry = getRandomElement( - meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) - ); - fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - } -}; - type RerollRandomModRequest = LetsGoGamblingRequest | AwDangitRequest; interface LetsGoGamblingRequest { @@ -85,20 +63,4 @@ interface AwDangitRequest { CommitReroll: boolean; } -interface IUnveiledRivenFingerprint { - compat: string; - lim: number; - lvl: number; - lvlReq: 0; - rerolls?: number; - pol: string; - buffs: IRivenStat[]; - curses: IRivenStat[]; -} - -interface IRivenStat { - Tag: string; - Value: number; -} - const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150]; diff --git a/src/helpers/rivenFingerprintHelper.ts b/src/helpers/rivenFingerprintHelper.ts new file mode 100644 index 00000000..c3742391 --- /dev/null +++ b/src/helpers/rivenFingerprintHelper.ts @@ -0,0 +1,38 @@ +import { IUpgrade } from "warframe-public-export-plus"; +import { getRandomElement } from "../services/rngService"; + +export interface IUnveiledRivenFingerprint { + compat: string; + lim: 0; + lvl: number; + lvlReq: number; + rerolls?: number; + pol: string; + buffs: IRivenStat[]; + curses: IRivenStat[]; +} + +interface IRivenStat { + Tag: string; + Value: number; +} + +export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenFingerprint): void => { + fingerprint.buffs = []; + const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 + const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff); + for (let i = 0; i != numBuffs; ++i) { + const buffIndex = Math.trunc(Math.random() * buffEntries.length); + const entry = buffEntries[buffIndex]; + fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + buffEntries.splice(buffIndex, 1); + } + + fingerprint.curses = []; + if (Math.random() < 0.5) { + const entry = getRandomElement( + meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) + ); + fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 2b3e511c..0a2c6716 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -8,6 +8,7 @@ import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootCo import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; +import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; @@ -133,6 +134,7 @@ apiRouter.post("/artifacts.php", artifactsController); apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); +apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/endlessXp.php", endlessXpController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); From 12743046478b220c1390c4598fea1d9064eaf8e9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Feb 2025 14:10:10 +0100 Subject: [PATCH 025/776] chore: fix type not matching reality --- src/types/inventoryTypes/inventoryTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e9375863..80795418 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -62,7 +62,7 @@ export interface IInventoryDatabase InfestedFoundry?: IInfestedFoundryDatabase; DialogueHistory?: IDialogueHistoryDatabase; KubrowPetEggs?: IKubrowPetEggDatabase[]; - PendingCoupon: IPendingCouponDatabase; + PendingCoupon?: IPendingCouponDatabase; } export interface IQuestKeyDatabase { @@ -320,7 +320,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CollectibleSeries: ICollectibleSery[]; LibraryAvailableDailyTaskInfo: ILibraryAvailableDailyTaskInfo; HasResetAccount: boolean; - PendingCoupon: IPendingCouponClient; + PendingCoupon?: IPendingCouponClient; Harvestable: boolean; DeathSquadable: boolean; EndlessXP?: IEndlessXpProgress[]; From 50d687e59a229eac4a5ecad2e8b9134de17b4a5d Mon Sep 17 00:00:00 2001 From: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:22:54 -0800 Subject: [PATCH 026/776] fix: re-enable giving ship features and mission rewards from Vors Prize after skipTutorial (#996) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/996 Co-authored-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> --- src/services/loginService.ts | 19 +++++++++---------- src/services/questService.ts | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 0432c33b..35b3feea 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -47,16 +47,15 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ activeShipId: shipId }); if (config.skipTutorial) { - // // Vor's Prize rewards - // const defaultFeatures = [ - // "/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem" - // ]; - // personalRooms.Ship.Features.push(...defaultFeatures); + // unlocked during Vor's Prize + const defaultFeatures = [ + "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem" + ]; + personalRooms.Ship.Features.push(...defaultFeatures); } await personalRooms.save(); }; diff --git a/src/services/questService.ts b/src/services/questService.ts index 630ca60a..b23d0e5d 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -3,8 +3,8 @@ import { isEmptyObject } from "@/src/helpers/general"; import { IMessage } from "@/src/models/inboxModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; -import { addKeyChainItems } from "@/src/services/inventoryService"; -import { getKeyChainMessage } from "@/src/services/itemDataService"; +import { addItem, addKeyChainItems } from "@/src/services/inventoryService"; +import { getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; import { IInventoryDatabase, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { HydratedDocument } from "mongoose"; @@ -125,6 +125,23 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest ChainStage: i }); } + + const missionName = chainStages[i].key; + if (missionName) { + const fixedLevelRewards = getLevelKeyRewards(missionName); + //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); + for (const reward of fixedLevelRewards) { + if (reward.rewardType == "RT_CREDITS") { + inventory.RegularCredits += reward.amount; + continue; + } + if (reward.rewardType == "RT_RESOURCE") { + await addItem(inventory, reward.itemType.replace("StoreItems/", ""), reward.amount); + } else { + await addItem(inventory, reward.itemType.replace("StoreItems/", "")); + } + } + } } inventory.ActiveQuest = ""; //TODO: handle quest completion items From d69cba6bef1c29039b61c98017d28ec4a972badc Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Feb 2025 05:27:50 -0800 Subject: [PATCH 027/776] chore: reuse inventory in claimCompletedRecipeController (#999) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/999 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/claimCompletedRecipeController.ts | 20 +++---------------- src/services/inventoryService.ts | 14 ------------- 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 779f4587..ec9087ba 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -7,14 +7,7 @@ import { getRecipe } from "@/src/services/itemDataService"; import { IOid } from "@/src/types/commonTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { - getInventory, - updateCurrency, - addItem, - addMiscItems, - addRecipes, - updateCurrencyByAccountId -} from "@/src/services/inventoryService"; +import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; export interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -37,7 +30,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = // } inventory.PendingRecipes.pull(pendingRecipe._id); - await inventory.save(); const recipe = getRecipe(pendingRecipe.ItemType); if (!recipe) { @@ -45,11 +37,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } if (req.query.cancel) { - const inventory = await getInventory(accountId); const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false); addMiscItems(inventory, recipe.ingredients); - await inventory.save(); + await inventory.save(); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. res.json({ ...currencyChanges, @@ -59,7 +50,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = logger.debug("Claiming Recipe", { recipe, pendingRecipe }); if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { - const inventory = await getInventory(accountId); inventory.PendingSpectreLoadouts ??= []; inventory.SpectreLoadouts ??= []; @@ -77,7 +67,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ); inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); - await inventory.save(); } } @@ -92,17 +81,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges }; - const inventory = await getInventory(accountId); addRecipes(inventory, recipeChanges); - await inventory.save(); } if (req.query.rush) { InventoryChanges = { ...InventoryChanges, - ...(await updateCurrencyByAccountId(recipe.skipBuildTimePrice, true, accountId)) + ...updateCurrency(inventory, recipe.skipBuildTimePrice, true) }; } - const inventory = await getInventory(accountId); InventoryChanges = { ...InventoryChanges, ...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 1cc12c5b..4b697185 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -650,20 +650,6 @@ export const updateCurrency = ( return currencyChanges; }; -export const updateCurrencyByAccountId = async ( - price: number, - usePremium: boolean, - accountId: string -): Promise => { - if (!isCurrencyTracked(usePremium)) { - return {}; - } - const inventory = await getInventory(accountId); - const currencyChanges = updateCurrency(inventory, price, usePremium); - await inventory.save(); - return currencyChanges; -}; - const standingLimitBinToInventoryKey: Record< Exclude, keyof IDailyAffiliations From ebb28d56d5b8ad8008a20e294f58417194d8dcc9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Feb 2025 05:28:43 -0800 Subject: [PATCH 028/776] feat: acquisition of resource extractor drones (#998) Related to #793 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/998 Co-authored-by: Sainan Co-committed-by: Sainan --- src/models/inventoryModels/inventoryModel.ts | 30 ++++++++++++++++++-- src/services/inventoryService.ts | 21 +++++++++++++- src/types/inventoryTypes/inventoryTypes.ts | 13 +++++++-- src/types/purchaseTypes.ts | 10 +++---- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 3ec3c363..0288cf42 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -68,7 +68,9 @@ import { ICalendarProgress, IPendingCouponDatabase, IPendingCouponClient, - ILibraryAvailableDailyTaskInfo + ILibraryAvailableDailyTaskInfo, + IDroneDatabase, + IDroneClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -349,6 +351,27 @@ const TypeXPItemSchema = new Schema( { _id: false } ); +const droneSchema = new Schema( + { + ItemType: String, + CurrentHP: Number, + RepairStart: { type: Date, default: undefined } + }, + { id: false } +); +droneSchema.set("toJSON", { + virtuals: true, + transform(_document, obj) { + const client = obj as IDroneClient; + const db = obj as IDroneDatabase; + + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + const challengeProgressSchema = new Schema( { Progress: Number, @@ -1148,8 +1171,8 @@ const inventorySchema = new Schema( CompletedSorties: [String], LastSortieReward: [Schema.Types.Mixed], - //Resource_Drone[Uselees stuff] - Drones: [Schema.Types.Mixed], + // Resource Extractor Drones + Drones: [droneSchema], //Active profile ico ActiveAvatarImageType: String, @@ -1299,6 +1322,7 @@ export type InventoryDocumentProps = { PendingRecipes: Types.DocumentArray; WeaponSkins: Types.DocumentArray; QuestKeys: Types.DocumentArray; + Drones: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 4b697185..3721f54d 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -24,7 +24,8 @@ import { IKubrowPetEggDatabase, IKubrowPetEggClient, ILibraryAvailableDailyTaskInfo, - ICalendarProgress + ICalendarProgress, + IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate } from "../types/genericUpdate"; import { @@ -38,6 +39,7 @@ import { IEquipmentClient, IEquipmentDatabase, IItemConfig } from "../types/inve import { ExportArcanes, ExportCustoms, + ExportDrones, ExportFlavour, ExportFusionBundles, ExportGear, @@ -336,6 +338,12 @@ export const addItem = async ( } }; } + if (typeName in ExportDrones) { + const inventoryChanges = addDrone(inventory, typeName); + return { + InventoryChanges: inventoryChanges + }; + } // Path-based duck typing switch (typeName.substr(1).split("/")[1]) { @@ -809,6 +817,17 @@ const addMotorcycle = ( return inventoryChanges; }; +const addDrone = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = inventory.Drones.push({ ItemType: typeName, CurrentHP: ExportDrones[typeName].durability }) - 1; + inventoryChanges.Drones ??= []; + inventoryChanges.Drones.push(inventory.Drones[index].toJSON()); + return inventoryChanges; +}; + //TODO: wrong id is not erroring export const addGearExpByCategory = ( inventory: TInventoryDatabaseDocument, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 80795418..f6e0c85a 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -39,6 +39,7 @@ export interface IInventoryDatabase | "DialogueHistory" | "KubrowPetEggs" | "PendingCoupon" + | "Drones" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -63,6 +64,7 @@ export interface IInventoryDatabase DialogueHistory?: IDialogueHistoryDatabase; KubrowPetEggs?: IKubrowPetEggDatabase[]; PendingCoupon?: IPendingCouponDatabase; + Drones: IDroneDatabase[]; } export interface IQuestKeyDatabase { @@ -258,7 +260,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Alignment: IAlignment; CompletedSorties: string[]; LastSortieReward: ILastSortieReward[]; - Drones: IDrone[]; + Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType: string; ShipDecorations: IConsumable[]; @@ -508,13 +510,20 @@ export interface IDiscoveredMarker { discoveryState: number[]; } -export interface IDrone { +export interface IDroneClient { ItemType: string; CurrentHP: number; ItemId: IOid; RepairStart?: IMongoDate; } +export interface IDroneDatabase { + ItemType: string; + CurrentHP: number; + _id: Types.ObjectId; + RepairStart?: Date; +} + export interface ITypeXPItem { ItemType: string; XP: number; diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 8dcdb4e8..f1e864be 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -1,5 +1,5 @@ import { IEquipmentClient } from "./inventoryTypes/commonInventoryTypes"; -import { IInfestedFoundryClient, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; +import { IDroneClient, IInfestedFoundryClient, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { PurchaseParams: IPurchaseParams; @@ -32,10 +32,10 @@ export type IInventoryChanges = { [_ in SlotNames]?: IBinChanges; } & { [_ in TEquipmentKey]?: IEquipmentClient[]; -} & ICurrencyChanges & { InfestedFoundry?: IInfestedFoundryClient } & Record< - string, - IBinChanges | number | object[] | IInfestedFoundryClient - >; +} & ICurrencyChanges & { + InfestedFoundry?: IInfestedFoundryClient; + Drones?: IDroneClient[]; + } & Record; export interface IAffiliationMods { Tag: string; From 9de57668ab961eb2c0646b1a44ff6cba731487dd Mon Sep 17 00:00:00 2001 From: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Date: Mon, 24 Feb 2025 06:14:47 -0800 Subject: [PATCH 029/776] fix: ensure quest progress exists (#1000) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1000 Co-authored-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/inventoryService.ts | 28 +++++++++++++------- src/services/questService.ts | 13 ++++++--- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0288cf42..af50028f 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -532,7 +532,7 @@ const questProgressSchema = new Schema( const questKeysSchema = new Schema( { - Progress: { type: [questProgressSchema], default: undefined }, + Progress: { type: [questProgressSchema], default: [] }, unlock: Boolean, Completed: Boolean, CustomData: String, diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 3721f54d..92fc007d 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -58,7 +58,7 @@ import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredIte import { toOid } from "../helpers/inventoryHelpers"; import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; import { addStartingGear } from "@/src/controllers/api/giveStartingGearController"; -import { completeQuest } from "@/src/services/questService"; +import { addQuestKey, completeQuest } from "@/src/services/questService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -327,16 +327,24 @@ export const addItem = async ( } if (typeName in ExportKeys) { // Note: "/Lotus/Types/Keys/" contains some EmailItems - inventory.QuestKeys.push({ ItemType: typeName }); - return { - InventoryChanges: { - QuestKeys: [ - { - ItemType: typeName - } - ] + const key = ExportKeys[typeName]; + + if (key.chainStages) { + const key = addQuestKey(inventory, { ItemType: typeName }); + if (key) { + return { InventoryChanges: { QuestKeys: [key] } }; } - }; + } else { + const key = { ItemType: typeName, ItemCount: quantity }; + + const index = inventory.LevelKeys.findIndex(levelKey => levelKey.ItemType == typeName); + if (index) { + inventory.LevelKeys[index].ItemCount += quantity; + } else { + inventory.LevelKeys.push(key); + } + return { InventoryChanges: { LevelKeys: [key] } }; + } } if (typeName in ExportDrones) { const inventoryChanges = addDrone(inventory, typeName); diff --git a/src/services/questService.ts b/src/services/questService.ts index b23d0e5d..e9899721 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -5,7 +5,12 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { createMessage } from "@/src/services/inboxService"; import { addItem, addKeyChainItems } from "@/src/services/inventoryService"; import { getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; -import { IInventoryDatabase, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; +import { + IInventoryDatabase, + IQuestKeyClient, + IQuestKeyDatabase, + IQuestStage +} from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { HydratedDocument } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; @@ -69,12 +74,14 @@ export const updateQuestStage = ( Object.assign(questStage, questStageUpdate); }; -export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase): void => { +export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase) => { if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) { logger.error(`quest key ${questKey.ItemType} already exists`); return; } - inventory.QuestKeys.push(questKey); + const index = inventory.QuestKeys.push(questKey); + + return inventory.QuestKeys[index - 1].toJSON(); }; export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string) => { From 045d93345882c31739e4b35a8de5cac745bddded Mon Sep 17 00:00:00 2001 From: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:50:07 -0800 Subject: [PATCH 030/776] fix: complete junction data and crash in vors prize mission four (#1001) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1001 Co-authored-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Co-committed-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> --- package-lock.json | 8 +- package.json | 2 +- src/services/itemDataService.ts | 21 ++-- src/services/missionInventoryUpdateService.ts | 107 ++++++++++++------ src/services/questService.ts | 28 +++-- 5 files changed, 105 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index a43d45a2..a46ffbde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.30", + "warframe-public-export-plus": "^0.5.35", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4093,9 +4093,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.32", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.32.tgz", - "integrity": "sha512-jO9i2Gzz9DWibiHlEO17D975ajs6KrTay8cS5I0GkUUe1XWVU8mML4b+IYCHzM4FWq1t6p2YPCGznQfknqvorg==" + "version": "0.5.35", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.35.tgz", + "integrity": "sha512-YLQP1n5sOV+PS5hfC4Kuoapa9gsqOy5Qy/E4EYfRV/xJBruFl3tPhbdbgFn3HhL2OBrgRJ8yzT5bjIvaHKhOCw==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index d26bee37..64eb538f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.30", + "warframe-public-export-plus": "^0.5.35", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index d68ef666..c18f3021 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -163,20 +163,21 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st }; export const getLevelKeyRewards = (levelKey: string) => { - const levelKeyData = ExportKeys[levelKey]; - if (!levelKeyData) { - const error = `LevelKey ${levelKey} not found`; - logger.error(error); - throw new Error(error); + if (!ExportKeys[levelKey]) { + throw new Error(`LevelKey ${levelKey} not found`); } - if (!levelKeyData.rewards) { - const error = `LevelKey ${levelKey} does not contain rewards`; - logger.error(error); - throw new Error(error); + const levelKeyRewards = ExportKeys[levelKey]?.missionReward; + const levelKeyRewards2 = ExportKeys[levelKey]?.rewards; + + if (!levelKeyRewards && !levelKeyRewards2) { + throw new Error(`LevelKey ${levelKey} does not contain either rewards1 or rewards2`); } - return levelKeyData.rewards; + return { + levelKeyRewards, + levelKeyRewards2 + }; }; export const getNode = (nodeName: string): IRegion => { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 5d7ecf16..2fd25030 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1,4 +1,10 @@ -import { ExportFusionBundles, ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus"; +import { + ExportFusionBundles, + ExportRegions, + ExportRewards, + IMissionReward as IMissionRewardExternal, + IReward +} from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, getRandomReward } from "@/src/services/rngService"; @@ -25,9 +31,8 @@ import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService"; import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import junctionRewards from "@/static/fixed_responses/junctionRewards.json"; -import { IJunctionRewards } from "@/src/types/commonTypes"; import { handleStoreItemAcquisition } from "./purchaseService"; +import { IMissionReward } from "../types/missionTypes"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -258,31 +263,38 @@ export const addMissionRewards = async ( if (levelKeyName) { const fixedLevelRewards = getLevelKeyRewards(levelKeyName); //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); - for (const reward of fixedLevelRewards) { - //quest stage completion credit rewards - if (reward.rewardType == "RT_CREDITS") { - inventory.RegularCredits += reward.amount; - missionCompletionCredits += reward.amount; - continue; + if (fixedLevelRewards.levelKeyRewards) { + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards); + } + if (fixedLevelRewards.levelKeyRewards2) { + for (const reward of fixedLevelRewards.levelKeyRewards2) { + //quest stage completion credit rewards + if (reward.rewardType == "RT_CREDITS") { + inventory.RegularCredits += reward.amount; + missionCompletionCredits += reward.amount; + continue; + } + MissionRewards.push({ + StoreItem: reward.itemType, + ItemCount: reward.rewardType === "RT_RESOURCE" ? reward.amount : 1 + }); } - MissionRewards.push({ - StoreItem: reward.itemType, - ItemCount: reward.rewardType === "RT_RESOURCE" ? reward.amount : 1 - }); } } - if (rewardInfo.node in junctionRewards) { - const junctionReward = (junctionRewards as IJunctionRewards)[rewardInfo.node]; - for (const item of junctionReward.items) { - MissionRewards.push({ - StoreItem: item.ItemType, - ItemCount: item.ItemCount - }); + if (missions) { + const node = getNode(missions.Tag); + + //node based credit rewards for mission completion + if (node.missionIndex !== 28) { + const levelCreditReward = getLevelCreditRewards(missions?.Tag); + missionCompletionCredits += levelCreditReward; + inventory.RegularCredits += levelCreditReward; + logger.debug(`levelCreditReward ${levelCreditReward}`); } - if (junctionReward.credits) { - inventory.RegularCredits += junctionReward.credits; - missionCompletionCredits += junctionReward.credits; + + if (node.missionReward) { + missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards); } } @@ -294,19 +306,6 @@ export const addMissionRewards = async ( combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges); } - //node based credit rewards for mission completion - if (missions) { - const node = getNode(missions.Tag); - - if (node.missionIndex !== 28) { - const levelCreditReward = getLevelCreditRewards(missions?.Tag); - missionCompletionCredits += levelCreditReward; - inventory.RegularCredits += levelCreditReward; - logger.debug(`levelCreditReward ${levelCreditReward}`); - } - } - - //creditBonus is not correct for mirage mission 3 const credits = addCredits(inventory, { missionCompletionCredits, missionDropCredits: creditDrops ?? 0, @@ -316,7 +315,7 @@ export const addMissionRewards = async ( return { inventoryChanges, MissionRewards, credits }; }; -//slightly inaccurate compared to official +//creditBonus is not entirely accurate. //TODO: consider ActiveBoosters export const addCredits = ( inventory: HydratedDocument, @@ -348,6 +347,40 @@ export const addCredits = ( return { ...finalCredits, DailyMissionBonus: true }; }; +export const addFixedLevelRewards = ( + rewards: IMissionRewardExternal, + inventory: TInventoryDatabaseDocument, + MissionRewards: IMissionReward[] +) => { + let missionBonusCredits = 0; + if (rewards.credits) { + missionBonusCredits += rewards.credits; + inventory.RegularCredits += rewards.credits; + } + if (rewards.items) { + for (const item of rewards.items) { + MissionRewards.push({ + StoreItem: `/Lotus/StoreItems${item.substring("Lotus/".length)}`, + ItemCount: 1 + }); + } + } + if (rewards.countedItems) { + for (const item of rewards.countedItems) { + MissionRewards.push({ + StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, + ItemCount: item.ItemCount + }); + } + } + if (rewards.countedStoreItems) { + for (const item of rewards.countedStoreItems) { + MissionRewards.push(item); + } + } + return missionBonusCredits; +}; + function getLevelCreditRewards(nodeName: string): number { const minEnemyLevel = getNode(nodeName).minEnemyLevel; diff --git a/src/services/questService.ts b/src/services/questService.ts index e9899721..f7b502d5 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -14,6 +14,7 @@ import { import { logger } from "@/src/utils/logger"; import { HydratedDocument } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; +import { addFixedLevelRewards } from "./missionInventoryUpdateService"; export interface IUpdateQuestRequest { QuestKeys: Omit[]; @@ -136,16 +137,25 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest const missionName = chainStages[i].key; if (missionName) { const fixedLevelRewards = getLevelKeyRewards(missionName); - //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); - for (const reward of fixedLevelRewards) { - if (reward.rewardType == "RT_CREDITS") { - inventory.RegularCredits += reward.amount; - continue; + //logger.debug(`fixedLevelRewards`, fixedLevelRewards); + if (fixedLevelRewards.levelKeyRewards) { + const missionRewards: { StoreItem: string; ItemCount: number }[] = []; + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); + + for (const reward of missionRewards) { + await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount); } - if (reward.rewardType == "RT_RESOURCE") { - await addItem(inventory, reward.itemType.replace("StoreItems/", ""), reward.amount); - } else { - await addItem(inventory, reward.itemType.replace("StoreItems/", "")); + } else if (fixedLevelRewards.levelKeyRewards2) { + for (const reward of fixedLevelRewards.levelKeyRewards2) { + if (reward.rewardType == "RT_CREDITS") { + inventory.RegularCredits += reward.amount; + continue; + } + if (reward.rewardType == "RT_RESOURCE") { + await addItem(inventory, reward.itemType.replace("StoreItems/", ""), reward.amount); + } else { + await addItem(inventory, reward.itemType.replace("StoreItems/", "")); + } } } } From 421164986a62bb6729b852c59a735e68c5435753 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:59:57 -0800 Subject: [PATCH 031/776] fix: don't throw an error if questKey already exists (#1003) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1003 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/inventoryService.ts | 5 ++--- src/services/questService.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 92fc007d..597a4bb7 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -331,9 +331,8 @@ export const addItem = async ( if (key.chainStages) { const key = addQuestKey(inventory, { ItemType: typeName }); - if (key) { - return { InventoryChanges: { QuestKeys: [key] } }; - } + if (!key) return { InventoryChanges: {} }; + return { InventoryChanges: { QuestKeys: [key] } }; } else { const key = { ItemType: typeName, ItemCount: quantity }; diff --git a/src/services/questService.ts b/src/services/questService.ts index f7b502d5..65d7e509 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -77,7 +77,7 @@ export const updateQuestStage = ( export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase) => { if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) { - logger.error(`quest key ${questKey.ItemType} already exists`); + logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`); return; } const index = inventory.QuestKeys.push(questKey); From 38b255d41a5ec55a36916f5a44433eff926a6933 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Feb 2025 20:56:27 -0800 Subject: [PATCH 032/776] chore: promote no-case-declarations lint to an error (#1006) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1006 --- .eslintrc | 2 +- src/services/inventoryService.ts | 27 ++++++++++--------- src/services/missionInventoryUpdateService.ts | 4 +-- src/services/statsService.ts | 6 +++-- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.eslintrc b/.eslintrc index f03e5269..6a62f9a7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,7 +23,7 @@ "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-loss-of-precision": "warn", "@typescript-eslint/no-unnecessary-condition": "warn", - "no-case-declarations": "warn", + "no-case-declarations": "error", "prettier/prettier": "error", "@typescript-eslint/semi": "error", "no-mixed-spaces-and-tabs": "error", diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 597a4bb7..ffb6570d 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -404,18 +404,21 @@ export const addItem = async ( switch (typeName.substr(1).split("/")[2]) { case "Mods": // Legendary Core case "CosmeticEnhancers": // Traumatic Peculiar - const changes = [ - { - ItemType: typeName, - ItemCount: quantity - } - ]; - addMods(inventory, changes); - return { - InventoryChanges: { - RawUpgrades: changes - } - }; + { + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addMods(inventory, changes); + return { + InventoryChanges: { + RawUpgrades: changes + } + }; + } + break; } break; } diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2fd25030..72ad8aa4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -208,14 +208,14 @@ export const addMissionInventoryUpdates = ( inventory.CompletedSorties.push(value); break; } - case "SeasonChallengeCompletions": + case "SeasonChallengeCompletions": { const processedCompletions = value.map(({ challenge, id }) => ({ challenge: challenge.substring(challenge.lastIndexOf("/") + 1), id })); - inventory.SeasonChallengeHistory.push(...processedCompletions); break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/services/statsService.ts b/src/services/statsService.ts index d520e842..c7a98399 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -105,7 +105,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "FIRE_WEAPON": case "HIT_ENTITY_ITEM": case "HEADSHOT_ITEM": - case "KILL_ENEMY_ITEM": + case "KILL_ENEMY_ITEM": { playerStats.Weapons ??= []; const statKey = { FIRE_WEAPON: "fired", @@ -126,10 +126,11 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: } } break; + } case "KILL_ENEMY": case "EXECUTE_ENEMY": - case "HEADSHOT": + case "HEADSHOT": { playerStats.Enemies ??= []; const enemyStatKey = { KILL_ENEMY: "kills", @@ -149,6 +150,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: } } break; + } case "DIE": playerStats.Enemies ??= []; From 2efe0df2f2065c27756277e22a9592de52889f5d Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Feb 2025 20:56:34 -0800 Subject: [PATCH 033/776] chore: fix some eslint warnings (#1007) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1007 --- .../api/giveStartingGearController.ts | 2 +- src/controllers/api/infestedFoundryController.ts | 2 +- .../api/projectionManagerController.ts | 1 + src/controllers/custom/getItemListsController.ts | 16 +++++----------- src/index.ts | 2 ++ src/models/inventoryModels/inventoryModel.ts | 4 ++-- src/services/importService.ts | 1 + src/services/inboxService.ts | 2 +- src/services/inventoryService.ts | 3 +++ src/services/missionInventoryUpdateService.ts | 6 +++--- src/services/questService.ts | 10 ++++++++-- src/services/saveLoadoutService.ts | 2 +- src/services/statsService.ts | 2 ++ src/types/inventoryTypes/inventoryTypes.ts | 6 +++--- src/utils/logger.ts | 4 ++-- 15 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index ef9be78f..118664b3 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -39,7 +39,7 @@ const awakeningRewards = [ export const addStartingGear = async ( inventory: HydratedDocument, startingGear: TPartialStartingGear | undefined = undefined -) => { +): Promise => { const { LongGuns, Pistols, Suits, Melee } = startingGear || { LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index f79587b5..acce1508 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -249,7 +249,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const suit = inventory.Suits.id(request.SuitId.$oid)!; inventory.Suits.pull(suit); const consumedSuit: IConsumedSuit = { s: suit.ItemType }; - if (suit.Configs && suit.Configs[0] && suit.Configs[0].pricol) { + if (suit.Configs[0] && suit.Configs[0].pricol) { consumedSuit.c = suit.Configs[0].pricol; } if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) { diff --git a/src/controllers/api/projectionManagerController.ts b/src/controllers/api/projectionManagerController.ts index 90c07f45..1f2554fb 100644 --- a/src/controllers/api/projectionManagerController.ts +++ b/src/controllers/api/projectionManagerController.ts @@ -50,6 +50,7 @@ const qualityKeywordToNumber: Record = { // e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"] const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => { const relic: IRelic | undefined = ExportRelics[typeName]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!relic) { throw new Error(`Unknown projection ${typeName}`); } diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 57792dd4..86488d12 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -39,17 +39,11 @@ const getItemListsController: RequestHandler = (req, response) => { res.miscitems = []; res.Syndicates = []; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { - if ( - item.productCategory == "Suits" || - item.productCategory == "SpaceSuits" || - item.productCategory == "MechSuits" - ) { - res[item.productCategory].push({ - uniqueName, - name: getString(item.name, lang), - exalted: item.exalted - }); - } + res[item.productCategory].push({ + uniqueName, + name: getString(item.name, lang), + exalted: item.exalted + }); } for (const [uniqueName, item] of Object.entries(ExportSentinels)) { if (item.productCategory == "Sentinels") { diff --git a/src/index.ts b/src/index.ts index acf55bdb..8bf614ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,10 @@ mongoose cert: fs.readFileSync("static/certs/cert.pem") }; + // eslint-disable-next-line @typescript-eslint/no-misused-promises http.createServer(app).listen(httpPort, () => { logger.info("HTTP server started on port " + httpPort); + // eslint-disable-next-line @typescript-eslint/no-misused-promises https.createServer(options, app).listen(httpsPort, () => { logger.info("HTTPS server started on port " + httpsPort); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index af50028f..0e82181c 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -808,7 +808,7 @@ detailsSchema.set("toJSON", { const EquipmentSchema = new Schema( { ItemType: String, - Configs: [ItemConfigSchema], + Configs: { type: [ItemConfigSchema], default: [] }, UpgradeVer: { type: Number, default: 101 }, XP: { type: Number, default: 0 }, Features: Number, @@ -1303,7 +1303,7 @@ inventorySchema.set("toJSON", { if (inventoryDatabase.GuildId) { inventoryResponse.GuildId = toOid(inventoryDatabase.GuildId); } - if (inventoryResponse.BlessingCooldown) { + if (inventoryDatabase.BlessingCooldown) { inventoryResponse.BlessingCooldown = toMongoDate(inventoryDatabase.BlessingCooldown); } } diff --git a/src/services/importService.ts b/src/services/importService.ts index 94fa1ef0..979221c8 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -50,6 +50,7 @@ const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => { UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry), CrewMembers: client.CrewMembers ? convertCrewShipMembers(client.CrewMembers) : undefined, Details: client.Details ? convertKubrowDetails(client.Details) : undefined, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition Configs: client.Configs ? client.Configs.map(obj => Object.fromEntries( diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index 895699f1..fa7a3812 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -27,7 +27,7 @@ export const deleteAllMessagesRead = async (accountId: string): Promise => await Inbox.deleteMany({ ownerId: accountId, r: true }); }; -export const createNewEventMessages = async (req: Request) => { +export const createNewEventMessages = async (req: Request): Promise => { const account = await getAccountForRequest(req); const latestEventMessageDate = account.LatestEventMessageDate; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ffb6570d..ca64cbe2 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -203,6 +203,7 @@ export const addItem = async ( const inventoryChanges = { ...addCrewShip(inventory, typeName), // fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition ...(!inventory.CrewShipHarnesses?.length ? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") : {}) @@ -525,12 +526,14 @@ export const addSentinel = ( sentinelName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportSentinels[sentinelName]?.defaultWeapon) { addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges); } const modsToGive: IRawUpgrade[] = []; const configs: IItemConfig[] = []; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportSentinels[sentinelName]?.defaultUpgrades) { const upgrades = []; for (const defaultUpgrade of ExportSentinels[sentinelName].defaultUpgrades) { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 72ad8aa4..0b9a16ba 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -80,7 +80,7 @@ export const creditBundles: Record = { export const addMissionInventoryUpdates = ( inventory: HydratedDocument, inventoryUpdates: IMissionInventoryUpdateRequest -) => { +): Partial | undefined => { //TODO: type this properly const inventoryChanges: Partial = {}; if (inventoryUpdates.MissionFailed === true) { @@ -287,7 +287,7 @@ export const addMissionRewards = async ( //node based credit rewards for mission completion if (node.missionIndex !== 28) { - const levelCreditReward = getLevelCreditRewards(missions?.Tag); + const levelCreditReward = getLevelCreditRewards(missions.Tag); missionCompletionCredits += levelCreditReward; inventory.RegularCredits += levelCreditReward; logger.debug(`levelCreditReward ${levelCreditReward}`); @@ -393,7 +393,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { const drops: IRngResult[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; - const rewardManifests = region.rewardManifests ?? []; + const rewardManifests = region.rewardManifests; let rotations: number[] = []; if (RewardInfo.VaultsCracked) { diff --git a/src/services/questService.ts b/src/services/questService.ts index 65d7e509..d0bdd9db 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -15,6 +15,7 @@ import { logger } from "@/src/utils/logger"; import { HydratedDocument } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; import { addFixedLevelRewards } from "./missionInventoryUpdateService"; +import { IInventoryChanges } from "../types/purchaseTypes"; export interface IUpdateQuestRequest { QuestKeys: Omit[]; @@ -64,6 +65,7 @@ export const updateQuestStage = ( const questStage = quest.Progress[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!questStage) { const questStageIndex = quest.Progress.push(questStageUpdate) - 1; if (questStageIndex !== ChainStage) { @@ -86,6 +88,7 @@ export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQu }; export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[questKey]?.chainStages; if (!chainStages) { @@ -164,7 +167,10 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest //TODO: handle quest completion items }; -export const giveKeyChainItem = async (inventory: TInventoryDatabaseDocument, keyChainInfo: IKeyChainRequest) => { +export const giveKeyChainItem = async ( + inventory: TInventoryDatabaseDocument, + keyChainInfo: IKeyChainRequest +): Promise => { const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); if (isEmptyObject(inventoryChanges)) { @@ -189,7 +195,7 @@ export const giveKeyChainMessage = async ( inventory: TInventoryDatabaseDocument, accountId: string, keyChainInfo: IKeyChainRequest -) => { +): Promise => { const keyChainMessage = getKeyChainMessage(keyChainInfo); const message = { diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 188b06ae..63533e43 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -148,7 +148,7 @@ export const handleInventoryItemConfigChange = async ( const itemEntries = equipment as IItemEntry; for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) { - const inventoryItem = inventory[equipmentName].find(item => item._id?.toString() === itemId); + const inventoryItem = inventory[equipmentName].id(itemId); if (!inventoryItem) { throw new Error(`inventory item ${equipmentName} not found with id ${itemId}`); diff --git a/src/services/statsService.ts b/src/services/statsService.ts index c7a98399..49bcea3f 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -58,6 +58,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: break; default: if (!ignoredCategories.includes(category)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!unknownCategories[action]) { unknownCategories[action] = []; } @@ -231,6 +232,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: default: if (!ignoredCategories.includes(category)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!unknownCategories[action]) { unknownCategories[action] = []; } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index f6e0c85a..e93f6b3c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -51,7 +51,7 @@ export interface IInventoryDatabase GuildId?: Types.ObjectId; PendingRecipes: IPendingRecipe[]; QuestKeys: IQuestKeyDatabase[]; - BlessingCooldown: Date; + BlessingCooldown?: Date; Ships: Types.ObjectId[]; WeaponSkins: IWeaponSkinDatabase[]; Upgrades: IUpgradeDatabase[]; @@ -300,7 +300,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu PlayedParkourTutorial: boolean; SubscribedToEmailsPersonalized: number; InfestedFoundry?: IInfestedFoundryClient; - BlessingCooldown: IMongoDate; + BlessingCooldown?: IMongoDate; CrewShipRawSalvage: IConsumable[]; CrewMembers: ICrewMember[]; LotusCustomization: ILotusCustomization; @@ -309,7 +309,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu LastInventorySync: IOid; NextRefill: IMongoDate; // Next time argon crystals will have a decay tick FoundToday?: IMiscItem[]; // for Argon Crystals - CustomMarkers: ICustomMarkers[]; + CustomMarkers?: ICustomMarkers[]; ActiveLandscapeTraps: any[]; EvolutionProgress?: IEvolutionProgress[]; RepVotes: any[]; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 52000727..f3873591 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -33,9 +33,9 @@ const consolelogFormat = format.printf(info => { colors: true }); - return `${info.timestamp} [${info.version}] ${info.level}: ${info.message} ${metadataString}`; + return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string} ${metadataString}`; } - return `${info.timestamp} [${info.version}] ${info.level}: ${info.message}`; + return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string}`; }); const fileFormat = format.combine( From bc0797884628564b0460ecd9abd752d3663fa42c Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Feb 2025 21:46:20 -0800 Subject: [PATCH 034/776] chore: use creditBundles map from PE+ (#1008) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1008 --- package-lock.json | 8 +++---- package.json | 2 +- src/services/inventoryService.ts | 6 +++--- src/services/missionInventoryUpdateService.ts | 21 ------------------- src/services/purchaseService.ts | 6 +++--- 5 files changed, 11 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index a46ffbde..478f3d4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.35", + "warframe-public-export-plus": "^0.5.36", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4093,9 +4093,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.35", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.35.tgz", - "integrity": "sha512-YLQP1n5sOV+PS5hfC4Kuoapa9gsqOy5Qy/E4EYfRV/xJBruFl3tPhbdbgFn3HhL2OBrgRJ8yzT5bjIvaHKhOCw==" + "version": "0.5.36", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.36.tgz", + "integrity": "sha512-FYZECqBSnynl6lQvcQyEqpnGW9l84wzusekhtwKjvg3280CYdn7g5x0Q9tOMhj1jpc/1tuY+akHtHa94sPtqKw==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 64eb538f..d3446687 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.35", + "warframe-public-export-plus": "^0.5.36", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ca64cbe2..81da3eb1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -44,6 +44,7 @@ import { ExportFusionBundles, ExportGear, ExportKeys, + ExportMisc, ExportRecipes, ExportResources, ExportSentinels, @@ -53,7 +54,6 @@ import { TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; -import { creditBundles } from "@/src/services/missionInventoryUpdateService"; import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { toOid } from "../helpers/inventoryHelpers"; import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; @@ -308,8 +308,8 @@ export const addItem = async ( }; } } - if (typeName in creditBundles) { - const creditsTotal = creditBundles[typeName] * quantity; + if (typeName in ExportMisc.creditBundles) { + const creditsTotal = ExportMisc.creditBundles[typeName] * quantity; inventory.RegularCredits += creditsTotal; return { InventoryChanges: { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 0b9a16ba..ad694802 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -51,27 +51,6 @@ const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { return getRandomReward(pool as IRngResult[]); }; -export const creditBundles: Record = { - "/Lotus/Types/PickUps/Credits/1500Credits": 1500, - "/Lotus/Types/PickUps/Credits/2000Credits": 2000, - "/Lotus/Types/PickUps/Credits/2500Credits": 2500, - "/Lotus/Types/PickUps/Credits/3000Credits": 3000, - "/Lotus/Types/PickUps/Credits/4000Credits": 4000, - "/Lotus/Types/PickUps/Credits/5000Credits": 5000, - "/Lotus/Types/PickUps/Credits/7500Credits": 7500, - "/Lotus/Types/PickUps/Credits/10000Credits": 10000, - "/Lotus/Types/PickUps/Credits/5000Hollars": 5000, - "/Lotus/Types/PickUps/Credits/7500Hollars": 7500, - "/Lotus/Types/PickUps/Credits/10000Hollars": 10000, - "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000, - "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000, - "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000, - "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000, - "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000, - "/Lotus/Types/StoreItems/CreditBundles/CreditBundleA": 50000, - "/Lotus/Types/StoreItems/CreditBundles/CreditBundleC": 175000 -}; - //type TMissionInventoryUpdateKeys = keyof IMissionInventoryUpdateRequest; //const ignoredInventoryUpdateKeys = ["FpsAvg", "FpsMax", "FpsMin", "FpsSamples"] satisfies TMissionInventoryUpdateKeys[]; // for keys with no meaning for this server //type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number]; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 7b59ab52..71d31de3 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -18,6 +18,7 @@ import { ExportBoosterPacks, ExportBundles, ExportGear, + ExportMisc, ExportResources, ExportSyndicates, ExportVendors, @@ -25,7 +26,6 @@ import { } from "warframe-public-export-plus"; import { config } from "./configService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; -import { creditBundles } from "./missionInventoryUpdateService"; export const getStoreItemCategory = (storeItem: string): string => { const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); @@ -335,8 +335,8 @@ const handleCreditBundlePurchase = async ( typeName: string, inventory: TInventoryDatabaseDocument ): Promise => { - if (typeName && typeName in creditBundles) { - const creditsAmount = creditBundles[typeName]; + if (typeName && typeName in ExportMisc.creditBundles) { + const creditsAmount = ExportMisc.creditBundles[typeName]; inventory.RegularCredits += creditsAmount; await inventory.save(); From c29bf6aab540395cf903ff8ad202e9c57d90fca4 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Feb 2025 21:46:30 -0800 Subject: [PATCH 035/776] chore: use PE+ for boosters (#1009) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1009 --- src/services/purchaseService.ts | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 71d31de3..36d91713 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -16,6 +16,7 @@ import { logger } from "@/src/utils/logger"; import worldState from "@/static/fixed_responses/worldState/worldState.json"; import { ExportBoosterPacks, + ExportBoosters, ExportBundles, ExportGear, ExportMisc, @@ -247,7 +248,7 @@ export const handleStoreItemAcquisition = async ( purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity); break; case "Boosters": - purchaseResponse = handleBoostersPurchase(internalName, inventory, durability); + purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); break; } } @@ -367,32 +368,18 @@ const handleTypesPurchase = async ( } }; -const boosterCollection = [ - "/Lotus/Types/Boosters/ResourceAmountBooster", - "/Lotus/Types/Boosters/AffinityBooster", - "/Lotus/Types/Boosters/ResourceDropChanceBooster", - "/Lotus/Types/Boosters/CreditBooster" -]; - -const boosterDuration: Record = { - COMMON: 3 * 86400, - UNCOMMON: 7 * 86400, - RARE: 30 * 86400, - LEGENDARY: 90 * 86400 -}; - const handleBoostersPurchase = ( boosterStoreName: string, inventory: TInventoryDatabaseDocument, durability: TRarity ): { InventoryChanges: IInventoryChanges } => { - const ItemType = boosterStoreName.replace("StoreItem", ""); - if (!boosterCollection.find(x => x == ItemType)) { - logger.error(`unknown booster type: ${ItemType}`); + if (!(boosterStoreName in ExportBoosters)) { + logger.error(`unknown booster type: ${boosterStoreName}`); return { InventoryChanges: {} }; } - const ExpiryDate = boosterDuration[durability]; + const ItemType = ExportBoosters[boosterStoreName].typeName; + const ExpiryDate = ExportMisc.boosterDurations[durability]; addBooster(ItemType, ExpiryDate, inventory); From 4d9e6a35abd205e2f1535709519c19f274e900e0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:36:10 -0800 Subject: [PATCH 036/776] fix: use correct reward manifest for arbitrations (#1004) Closes #939 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1004 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index ad694802..61ade8b2 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -372,7 +372,10 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { const drops: IRngResult[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; - const rewardManifests = region.rewardManifests; + const rewardManifests: string[] = + RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB" + ? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"] + : region.rewardManifests; let rotations: number[] = []; if (RewardInfo.VaultsCracked) { From f672f05db955c496fe5698fa8ea475a8c049213d Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:38:17 -0800 Subject: [PATCH 037/776] fix: handle bundles being given to addItems (#1005) This is needed for the Hex noggles email attachment Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1005 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 7 ++++++ src/services/purchaseService.ts | 42 ++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 81da3eb1..6bce6e5e 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -38,6 +38,7 @@ import { getExalted, getKeyChainItems } from "@/src/services/itemDataService"; import { IEquipmentClient, IEquipmentDatabase, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; import { ExportArcanes, + ExportBundles, ExportCustoms, ExportDrones, ExportFlavour, @@ -59,6 +60,7 @@ import { toOid } from "../helpers/inventoryHelpers"; import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; import { addStartingGear } from "@/src/controllers/api/giveStartingGearController"; import { addQuestKey, completeQuest } from "@/src/services/questService"; +import { handleBundleAcqusition } from "./purchaseService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -157,6 +159,11 @@ export const addItem = async ( typeName: string, quantity: number = 1 ): Promise<{ InventoryChanges: IInventoryChanges }> => { + // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. + if (typeName in ExportBundles) { + return { InventoryChanges: await handleBundleAcqusition(typeName, inventory, quantity) }; + } + // Strict typing if (typeName in ExportRecipes) { const recipeChanges = [ diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 36d91713..76a6d111 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -200,6 +200,31 @@ const handleItemPrices = ( } }; +export const handleBundleAcqusition = async ( + storeItemName: string, + inventory: TInventoryDatabaseDocument, + quantity: number = 1, + inventoryChanges: IInventoryChanges = {} +): Promise => { + const bundle = ExportBundles[storeItemName]; + logger.debug("acquiring bundle", bundle); + for (const component of bundle.components) { + combineInventoryChanges( + inventoryChanges, + ( + await handleStoreItemAcquisition( + component.typeName, + inventory, + component.purchaseQuantity * quantity, + component.durability, + true + ) + ).InventoryChanges + ); + } + return inventoryChanges; +}; + export const handleStoreItemAcquisition = async ( storeItemName: string, inventory: TInventoryDatabaseDocument, @@ -212,22 +237,7 @@ export const handleStoreItemAcquisition = async ( }; logger.debug(`handling acquision of ${storeItemName}`); if (storeItemName in ExportBundles) { - const bundle = ExportBundles[storeItemName]; - logger.debug("acquiring bundle", bundle); - for (const component of bundle.components) { - combineInventoryChanges( - purchaseResponse.InventoryChanges, - ( - await handleStoreItemAcquisition( - component.typeName, - inventory, - component.purchaseQuantity * quantity, - component.durability, - true - ) - ).InventoryChanges - ); - } + await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges); } else { const storeCategory = getStoreItemCategory(storeItemName); const internalName = storeItemName.replace("/StoreItems", ""); From 39f0f7de9a64ebf96710eb2832f9a89539e1df0c Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:38:47 -0800 Subject: [PATCH 038/776] feat: cracking relics in non-endless missions (#1010) Closes #415 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1010 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/getVoidProjectionRewardsController.ts | 84 +++---------------- src/helpers/relicHelper.ts | 60 +++++++++++++ src/services/missionInventoryUpdateService.ts | 13 ++- src/types/requestTypes.ts | 19 +++++ 4 files changed, 103 insertions(+), 73 deletions(-) create mode 100644 src/helpers/relicHelper.ts diff --git a/src/controllers/api/getVoidProjectionRewardsController.ts b/src/controllers/api/getVoidProjectionRewardsController.ts index 9a577cf5..3a09f4ba 100644 --- a/src/controllers/api/getVoidProjectionRewardsController.ts +++ b/src/controllers/api/getVoidProjectionRewardsController.ts @@ -1,77 +1,31 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { crackRelic } from "@/src/helpers/relicHelper"; +import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { getRandomWeightedReward2 } from "@/src/services/rngService"; -import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; -import { logger } from "@/src/utils/logger"; +import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; -import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const data = getJSONfromString(String(req.body)); + + if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) { + const inventory = await getInventory(accountId); + await crackRelic(inventory, data.ParticipantInfo); + await inventory.save(); + } + const response: IVoidProjectionRewardResponse = { CurrentWave: data.CurrentWave, ParticipantInfo: data.ParticipantInfo, DifficultyTier: data.DifficultyTier }; - if (data.ParticipantInfo.QualifiesForReward) { - const relic = ExportRelics[data.ParticipantInfo.VoidProjection]; - const weights = refinementToWeights[relic.quality]; - logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); - const reward = getRandomWeightedReward2( - ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics - weights - )!; - logger.debug(`relic rolled`, reward); - response.ParticipantInfo.Reward = reward.type; - - const inventory = await getInventory(accountId); - // Remove relic - addMiscItems(inventory, [ - { - ItemType: data.ParticipantInfo.VoidProjection, - ItemCount: -1 - } - ]); - // Give reward - await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount); - await inventory.save(); - } res.json(response); }; -const refinementToWeights = { - VPQ_BRONZE: { - COMMON: 0.76, - UNCOMMON: 0.22, - RARE: 0.02, - LEGENDARY: 0 - }, - VPQ_SILVER: { - COMMON: 0.7, - UNCOMMON: 0.26, - RARE: 0.04, - LEGENDARY: 0 - }, - VPQ_GOLD: { - COMMON: 0.6, - UNCOMMON: 0.34, - RARE: 0.06, - LEGENDARY: 0 - }, - VPQ_PLATINUM: { - COMMON: 0.5, - UNCOMMON: 0.4, - RARE: 0.1, - LEGENDARY: 0 - } -}; - interface IVoidProjectionRewardRequest { CurrentWave: number; - ParticipantInfo: IParticipantInfo; + ParticipantInfo: IVoidTearParticipantInfo; VoidTier: string; DifficultyTier: number; VoidProjectionRemovalHash: string; @@ -79,20 +33,6 @@ interface IVoidProjectionRewardRequest { interface IVoidProjectionRewardResponse { CurrentWave: number; - ParticipantInfo: IParticipantInfo; + ParticipantInfo: IVoidTearParticipantInfo; DifficultyTier: number; } - -interface IParticipantInfo { - AccountId: string; - Name: string; - ChosenRewardOwner: string; - MissionHash: string; - VoidProjection: string; - Reward: string; - QualifiesForReward: boolean; - HaveRewardResponse: boolean; - RewardsMultiplier: number; - RewardProjection: string; - HardModeReward: ITypeCount; -} diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts new file mode 100644 index 00000000..6e28aef0 --- /dev/null +++ b/src/helpers/relicHelper.ts @@ -0,0 +1,60 @@ +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; +import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; +import { getRandomWeightedReward2 } from "@/src/services/rngService"; +import { logger } from "@/src/utils/logger"; +import { addMiscItems } from "@/src/services/inventoryService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; + +export const crackRelic = async ( + inventory: TInventoryDatabaseDocument, + participant: IVoidTearParticipantInfo +): Promise => { + const relic = ExportRelics[participant.VoidProjection]; + const weights = refinementToWeights[relic.quality]; + logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); + const reward = getRandomWeightedReward2( + ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics + weights + )!; + logger.debug(`relic rolled`, reward); + participant.Reward = reward.type; + + // Remove relic + addMiscItems(inventory, [ + { + ItemType: participant.VoidProjection, + ItemCount: -1 + } + ]); + + // Give reward + await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount); +}; + +const refinementToWeights = { + VPQ_BRONZE: { + COMMON: 0.76, + UNCOMMON: 0.22, + RARE: 0.02, + LEGENDARY: 0 + }, + VPQ_SILVER: { + COMMON: 0.7, + UNCOMMON: 0.26, + RARE: 0.04, + LEGENDARY: 0 + }, + VPQ_GOLD: { + COMMON: 0.6, + UNCOMMON: 0.34, + RARE: 0.06, + LEGENDARY: 0 + }, + VPQ_PLATINUM: { + COMMON: 0.5, + UNCOMMON: 0.4, + RARE: 0.1, + LEGENDARY: 0 + } +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 61ade8b2..5bd509be 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -33,6 +33,7 @@ import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { handleStoreItemAcquisition } from "./purchaseService"; import { IMissionReward } from "../types/missionTypes"; +import { crackRelic } from "@/src/helpers/relicHelper"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -221,7 +222,8 @@ export const addMissionRewards = async ( RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions, - RegularCredits: creditDrops + RegularCredits: creditDrops, + VoidTearParticipantsCurrWave: voidTearWave }: IMissionInventoryUpdateRequest ) => { if (!rewardInfo) { @@ -291,6 +293,15 @@ export const addMissionRewards = async ( rngRewardCredits: inventoryChanges.RegularCredits ?? 0 }); + if ( + voidTearWave && + voidTearWave.Participants[0].QualifiesForReward && + !voidTearWave.Participants[0].HaveRewardResponse + ) { + await crackRelic(inventory, voidTearWave.Participants[0]); + MissionRewards.push({ StoreItem: voidTearWave.Participants[0].Reward, ItemCount: 1 }); + } + return { inventoryChanges, MissionRewards, credits }; }; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index e7706d01..0220240a 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -87,6 +87,11 @@ export type IMissionInventoryUpdateRequest = { PlayerSkillGains: IPlayerSkills; CustomMarkers?: ICustomMarkers[]; LoreFragmentScans?: ILoreFragmentScan[]; + VoidTearParticipantsCurrWave?: { + Wave: number; + IsFinalWave: boolean; + Participants: IVoidTearParticipantInfo[]; + }; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; @@ -136,3 +141,17 @@ export interface IUnlockShipFeatureRequest { KeyChain: string; ChainStage: number; } + +export interface IVoidTearParticipantInfo { + AccountId: string; + Name: string; + ChosenRewardOwner: string; + MissionHash: string; + VoidProjection: string; + Reward: string; + QualifiesForReward: boolean; + HaveRewardResponse: boolean; + RewardsMultiplier: number; + RewardProjection: string; + HardModeReward: ITypeCount; +} From 3d82fee99ea5e2e8948fc33fd25da60cef4ca475 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:39:59 -0800 Subject: [PATCH 039/776] feat: give additionalItems for weapons (#1011) Closes #1002 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1011 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6bce6e5e..7977818a 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -292,6 +292,11 @@ export const addItem = async ( const weapon = ExportWeapons[typeName]; if (weapon.totalDamage != 0) { const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName); + if (weapon.additionalItems) { + for (const item of weapon.additionalItems) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + } + } updateSlots(inventory, InventorySlot.WEAPONS, 0, 1); return { InventoryChanges: { From e6ec144f1f7f1bb9343497cd2fd32251472f36de Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:41:14 -0800 Subject: [PATCH 040/776] feat: handle defaultUpgrades for moas and hounds (#1012) Closes #997 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1012 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/modularWeaponCraftingController.ts | 16 ++++++-- src/services/inventoryService.ts | 39 ++++++++++++------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index d70f48ca..4be0d980 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -2,7 +2,14 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; -import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService"; +import { + getInventory, + updateCurrency, + addEquipment, + addMiscItems, + applyDefaultUpgrades +} from "@/src/services/inventoryService"; +import { ExportWeapons } from "warframe-public-export-plus"; const modularWeaponTypes: Record = { "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", @@ -36,8 +43,11 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const category = modularWeaponTypes[data.WeaponType]; const inventory = await getInventory(accountId); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const configs = applyDefaultUpgrades(inventory, ExportWeapons[data.Parts[0]]?.defaultUpgrades); + // Give weapon - const weapon = addEquipment(inventory, category, data.WeaponType, data.Parts); + const inventoryChanges = addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }); // Remove credits & parts const miscItemChanges = []; @@ -58,8 +68,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) // Tell client what we did res.json({ InventoryChanges: { + ...inventoryChanges, ...currencyChanges, - [category]: [weapon], MiscItems: miscItemChanges } }); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 7977818a..1f895825 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -52,6 +52,7 @@ import { ExportSyndicates, ExportUpgrades, ExportWeapons, + IDefaultUpgrade, TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; @@ -532,6 +533,28 @@ export const addItems = async ( return inventoryChanges; }; +export const applyDefaultUpgrades = ( + inventory: TInventoryDatabaseDocument, + defaultUpgrades: IDefaultUpgrade[] | undefined +): IItemConfig[] => { + const modsToGive: IRawUpgrade[] = []; + const configs: IItemConfig[] = []; + if (defaultUpgrades) { + const upgrades = []; + for (const defaultUpgrade of defaultUpgrades) { + modsToGive.push({ ItemType: defaultUpgrade.ItemType, ItemCount: 1 }); + if (defaultUpgrade.Slot != -1) { + upgrades[defaultUpgrade.Slot] = defaultUpgrade.ItemType; + } + } + if (upgrades.length != 0) { + configs.push({ Upgrades: upgrades }); + } + } + addMods(inventory, modsToGive); + return configs; +}; + //TODO: maybe genericMethod for all the add methods, they share a lot of logic export const addSentinel = ( inventory: TInventoryDatabaseDocument, @@ -543,23 +566,9 @@ export const addSentinel = ( addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges); } - const modsToGive: IRawUpgrade[] = []; - const configs: IItemConfig[] = []; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (ExportSentinels[sentinelName]?.defaultUpgrades) { - const upgrades = []; - for (const defaultUpgrade of ExportSentinels[sentinelName].defaultUpgrades) { - modsToGive.push({ ItemType: defaultUpgrade.ItemType, ItemCount: 1 }); - if (defaultUpgrade.Slot != -1) { - upgrades[defaultUpgrade.Slot] = defaultUpgrade.ItemType; - } - } - if (upgrades.length != 0) { - configs.push({ Upgrades: upgrades }); - } - } + const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades); - addMods(inventory, modsToGive); const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0 }) - 1; inventoryChanges.Sentinels ??= []; inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON()); From b5b088249c0cbaa74b4034fef1042968d23477c6 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:41:45 -0800 Subject: [PATCH 041/776] fix: ignore empty mission tag in missionInventoryUpdate (#1015) Fixes #1013 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1015 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 5bd509be..b4e9c41f 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -263,7 +263,10 @@ export const addMissionRewards = async ( } } - if (missions) { + if ( + missions && + missions.Tag != "" // #1013 + ) { const node = getNode(missions.Tag); //node based credit rewards for mission completion From 93afc2645c4c57cfa859c1997bb99550cfd5e5c5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 04:42:49 -0800 Subject: [PATCH 042/776] fix: items from enemy caches not showing "identified" (#1016) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1016 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 14 ++++++-------- src/types/missionTypes.ts | 2 ++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b4e9c41f..d0e82376 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -233,9 +233,7 @@ export const addMissionRewards = async ( } //TODO: check double reward merging - const MissionRewards = getRandomMissionDrops(rewardInfo).map(drop => { - return { StoreItem: drop.type, ItemCount: drop.itemCount }; - }); + const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; @@ -382,8 +380,8 @@ function getLevelCreditRewards(nodeName: string): number { //TODO: get dark sektor fixed credit rewards and railjack bonus } -function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { - const drops: IRngResult[] = []; +function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] { + const drops: IMissionReward[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; const rewardManifests: string[] = @@ -408,7 +406,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { const rotationRewards = table[rotation]; const drop = getRandomRewardByChance(rotationRewards); if (drop) { - drops.push(drop); + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } } }); @@ -418,7 +416,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) { const drop = getRandomRewardByChance(deck[rotation]); if (drop) { - drops.push(drop); + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true }); } } } @@ -437,7 +435,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { const drop = getRandomRewardByChance(deck[rotation]); if (drop) { - drops.push(drop); + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } } } diff --git a/src/types/missionTypes.ts b/src/types/missionTypes.ts index 8e79e8d8..c8cab373 100644 --- a/src/types/missionTypes.ts +++ b/src/types/missionTypes.ts @@ -8,4 +8,6 @@ export interface IMissionReward { ItemCount: number; TweetText?: string; ProductCategory?: string; + FromEnemyCache?: boolean; + IsStrippedItem?: boolean; } From a27f1c5e017dcbcd1527138b5629b188d753f1c4 Mon Sep 17 00:00:00 2001 From: VampireKitten Date: Tue, 25 Feb 2025 10:08:27 -0800 Subject: [PATCH 043/776] fix: converting storeitems in missionRewards (#1017) Fixes the acquisition of blueprints as rewards, such as those rewarded by the Junctions. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1017 Co-authored-by: VampireKitten Co-committed-by: VampireKitten --- src/services/missionInventoryUpdateService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index d0e82376..2d393e22 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -351,7 +351,7 @@ export const addFixedLevelRewards = ( if (rewards.items) { for (const item of rewards.items) { MissionRewards.push({ - StoreItem: `/Lotus/StoreItems${item.substring("Lotus/".length)}`, + StoreItem: item.includes(`/StoreItems/`) ? item : `/Lotus/StoreItems${item.substring("Lotus/".length)}`, ItemCount: 1 }); } @@ -359,7 +359,9 @@ export const addFixedLevelRewards = ( if (rewards.countedItems) { for (const item of rewards.countedItems) { MissionRewards.push({ - StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, + StoreItem: item.ItemType.includes(`/StoreItems/`) + ? item.ItemType + : `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, ItemCount: item.ItemCount }); } From 3945359e7d65ad9138b2b683d2652c0d26f370b4 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 16:58:07 -0800 Subject: [PATCH 044/776] chore: simplify conversion of missionReward from PE+ (#1018) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1018 --- src/services/missionInventoryUpdateService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2d393e22..7b150d27 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -351,7 +351,7 @@ export const addFixedLevelRewards = ( if (rewards.items) { for (const item of rewards.items) { MissionRewards.push({ - StoreItem: item.includes(`/StoreItems/`) ? item : `/Lotus/StoreItems${item.substring("Lotus/".length)}`, + StoreItem: item, ItemCount: 1 }); } @@ -359,9 +359,7 @@ export const addFixedLevelRewards = ( if (rewards.countedItems) { for (const item of rewards.countedItems) { MissionRewards.push({ - StoreItem: item.ItemType.includes(`/StoreItems/`) - ? item.ItemType - : `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, + StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, ItemCount: item.ItemCount }); } From c13cf7081460d1798ce4dbb5ec6e11dbe842afb7 Mon Sep 17 00:00:00 2001 From: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Date: Wed, 26 Feb 2025 02:26:22 +0100 Subject: [PATCH 045/776] fix: update vor's prize completion rewards --- static/fixed_responses/questCompletionRewards.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/static/fixed_responses/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json index 9d727f64..d0f692aa 100644 --- a/static/fixed_responses/questCompletionRewards.json +++ b/static/fixed_responses/questCompletionRewards.json @@ -1,9 +1,5 @@ { "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain": [ - { - "ItemType": "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain", - "ItemCount": 1 - }, { "ItemType": "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", "ItemCount": 1 From 8fea608b76672731342261a7064c3faf0c426c8d Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 17:31:16 -0800 Subject: [PATCH 046/776] fix: fill upgrades array with empty strings (#1023) Otherwise the client will "LogBug: (Invalid UpgradeId)" and may crash/raise an interrupt Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1023 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 1f895825..66829381 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -544,6 +544,9 @@ export const applyDefaultUpgrades = ( for (const defaultUpgrade of defaultUpgrades) { modsToGive.push({ ItemType: defaultUpgrade.ItemType, ItemCount: 1 }); if (defaultUpgrade.Slot != -1) { + while (upgrades.length < defaultUpgrade.Slot) { + upgrades.push(""); + } upgrades[defaultUpgrade.Slot] = defaultUpgrade.ItemType; } } From 2b8da4af603ae86d4eb8ca0ba8a79c4f963e0c1e Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 17:31:24 -0800 Subject: [PATCH 047/776] fix: increment LoreFragmentScans Progress when already present (#1022) Fixes #1021 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1022 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7b150d27..da505c05 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -176,8 +176,13 @@ export const addMissionInventoryUpdates = ( break; } case "LoreFragmentScans": - value.forEach(x => { - inventory.LoreFragmentScans.push(x); + value.forEach(clientFragment => { + const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); + if (fragment) { + fragment.Progress += clientFragment.Progress; + } else { + inventory.LoreFragmentScans.push(clientFragment); + } }); break; case "SyndicateId": { From d7628d46e983e2a6a87a9fb5a96fa562912fd8c7 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 17:31:33 -0800 Subject: [PATCH 048/776] fix: acquisition of CrewShipWeaponSkins (#1019) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1019 Co-authored-by: Sainan Co-committed-by: Sainan --- package-lock.json | 8 +++---- package.json | 2 +- src/models/inventoryModels/inventoryModel.ts | 3 ++- src/services/inventoryService.ts | 23 +++++++++++++++++--- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 478f3d4e..72f2bd61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.36", + "warframe-public-export-plus": "^0.5.37", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4093,9 +4093,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.36", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.36.tgz", - "integrity": "sha512-FYZECqBSnynl6lQvcQyEqpnGW9l84wzusekhtwKjvg3280CYdn7g5x0Q9tOMhj1jpc/1tuY+akHtHa94sPtqKw==" + "version": "0.5.37", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.37.tgz", + "integrity": "sha512-atpTQ0IV0HF17rO2+Z+Vdv8nnnUxh5HhkcXBjc5iwY8tlvRgRWwHemq4PdA1bxR4tYFU5xoYjlgDe5D8ZfM4ew==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index d3446687..5bbcc52b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.36", + "warframe-public-export-plus": "^0.5.37", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0e82181c..a4c6557b 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1084,7 +1084,7 @@ const inventorySchema = new Schema( //Default RailJack CrewShipAmmo: [typeCountSchema], CrewShipWeapons: [Schema.Types.Mixed], - CrewShipWeaponSkins: [Schema.Types.Mixed], + CrewShipWeaponSkins: [upgradeSchema], //NPC Crew and weapon CrewMembers: [Schema.Types.Mixed], @@ -1323,6 +1323,7 @@ export type InventoryDocumentProps = { WeaponSkins: Types.DocumentArray; QuestKeys: Types.DocumentArray; Drones: Types.DocumentArray; + CrewShipWeaponSkins: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 66829381..917a3934 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -25,7 +25,8 @@ import { IKubrowPetEggClient, ILibraryAvailableDailyTaskInfo, ICalendarProgress, - IDroneClient + IDroneClient, + IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate } from "../types/genericUpdate"; import { @@ -254,8 +255,11 @@ export const addItem = async ( } } if (typeName in ExportCustoms) { - const inventoryChanges = addSkin(inventory, typeName); - return { InventoryChanges: inventoryChanges }; + if (ExportCustoms[typeName].productCategory == "CrewShipWeaponSkins") { + return { InventoryChanges: addCrewShipWeaponSkin(inventory, typeName) }; + } else { + return { InventoryChanges: addSkin(inventory, typeName) }; + } } if (typeName in ExportFlavour) { const inventoryChanges = addCustomization(inventory, typeName); @@ -812,6 +816,19 @@ export const addSkin = ( return inventoryChanges; }; +const addCrewShipWeaponSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1; + inventoryChanges.CrewShipWeaponSkins ??= []; + (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push( + inventory.CrewShipWeaponSkins[index].toJSON() + ); + return inventoryChanges; +}; + const addCrewShip = ( inventory: TInventoryDatabaseDocument, typeName: string, From 28a36052d98083754f33bf2f83f4c689a404eef4 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 17:31:52 -0800 Subject: [PATCH 049/776] feat: daily synthesis (#1014) Closes #386 Closes #533 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1014 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/abandonLibraryDailyTaskController.ts | 11 ++ .../claimLibraryDailyTaskRewardController.ts | 31 ++++ src/controllers/api/inventoryController.ts | 5 +- .../api/startLibraryDailyTaskController.ts | 11 ++ src/models/inventoryModels/inventoryModel.ts | 8 +- src/routes/api.ts | 6 + src/services/inventoryService.ts | 27 ++-- src/services/missionInventoryUpdateService.ts | 16 ++ src/types/inventoryTypes/inventoryTypes.ts | 6 +- src/types/requestTypes.ts | 6 + static/fixed_responses/libraryDailyTasks.json | 148 ++++++++++++++++++ 11 files changed, 259 insertions(+), 16 deletions(-) create mode 100644 src/controllers/api/abandonLibraryDailyTaskController.ts create mode 100644 src/controllers/api/claimLibraryDailyTaskRewardController.ts create mode 100644 src/controllers/api/startLibraryDailyTaskController.ts create mode 100644 static/fixed_responses/libraryDailyTasks.json diff --git a/src/controllers/api/abandonLibraryDailyTaskController.ts b/src/controllers/api/abandonLibraryDailyTaskController.ts new file mode 100644 index 00000000..ac515609 --- /dev/null +++ b/src/controllers/api/abandonLibraryDailyTaskController.ts @@ -0,0 +1,11 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const abandonLibraryDailyTaskController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + inventory.LibraryActiveDailyTaskInfo = undefined; + await inventory.save(); + res.status(200).end(); +}; diff --git a/src/controllers/api/claimLibraryDailyTaskRewardController.ts b/src/controllers/api/claimLibraryDailyTaskRewardController.ts new file mode 100644 index 00000000..6d8e1d41 --- /dev/null +++ b/src/controllers/api/claimLibraryDailyTaskRewardController.ts @@ -0,0 +1,31 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + + const rewardQuantity = inventory.LibraryActiveDailyTaskInfo!.RewardQuantity; + const rewardStanding = inventory.LibraryActiveDailyTaskInfo!.RewardStanding; + inventory.LibraryActiveDailyTaskInfo = undefined; + inventory.LibraryAvailableDailyTaskInfo = undefined; + + let syndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate"); + if (!syndicate) { + syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: "LibrarySyndicate", Standing: 0 }) - 1]; + } + syndicate.Standing += rewardStanding; + + inventory.FusionPoints += 80 * rewardQuantity; + await inventory.save(); + + res.json({ + RewardItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle", + RewardQuantity: rewardQuantity, + StandingAwarded: rewardStanding, + InventoryChanges: { + FusionPoints: 80 * rewardQuantity + } + }); +}; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 82b55ff5..0b33eba4 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -14,7 +14,7 @@ import { ExportVirtuals } from "warframe-public-export-plus"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; -import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; +import { allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; export const inventoryController: RequestHandler = async (request, response) => { const account = await getAccountForRequest(request); @@ -36,6 +36,9 @@ export const inventoryController: RequestHandler = async (request, response) => inventory[key] = 16000 + inventory.PlayerLevel * 500; } inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; + + inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); + await inventory.save(); } diff --git a/src/controllers/api/startLibraryDailyTaskController.ts b/src/controllers/api/startLibraryDailyTaskController.ts new file mode 100644 index 00000000..e8b8425b --- /dev/null +++ b/src/controllers/api/startLibraryDailyTaskController.ts @@ -0,0 +1,11 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const startLibraryDailyTaskController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + inventory.LibraryActiveDailyTaskInfo = inventory.LibraryAvailableDailyTaskInfo; + await inventory.save(); + res.json(inventory.LibraryAvailableDailyTaskInfo); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a4c6557b..5ddfbdec 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -68,7 +68,7 @@ import { ICalendarProgress, IPendingCouponDatabase, IPendingCouponClient, - ILibraryAvailableDailyTaskInfo, + ILibraryDailyTaskInfo, IDroneDatabase, IDroneClient } from "../../types/inventoryTypes/inventoryTypes"; @@ -950,11 +950,12 @@ pendingCouponSchema.set("toJSON", { } }); -const libraryAvailableDailyTaskInfoSchema = new Schema( +const libraryDailyTaskInfoSchema = new Schema( { EnemyTypes: [String], EnemyLocTag: String, EnemyIcon: String, + Scans: Number, ScansRequired: Number, RewardStoreItem: String, RewardQuantity: Number, @@ -1209,7 +1210,8 @@ const inventorySchema = new Schema( //Cephalon Simaris Entries Example:"TargetType"+"Scans"(1-10)+"Completed": true|false LibraryPersonalProgress: [Schema.Types.Mixed], //Cephalon Simaris Daily Task - LibraryAvailableDailyTaskInfo: libraryAvailableDailyTaskInfoSchema, + LibraryAvailableDailyTaskInfo: libraryDailyTaskInfoSchema, + LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema, //https://warframe.fandom.com/wiki/Invasion InvasionChainProgress: [Schema.Types.Mixed], diff --git a/src/routes/api.ts b/src/routes/api.ts index 0a2c6716..335c59df 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,4 +1,5 @@ import express from "express"; +import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -7,6 +8,7 @@ import { artifactsController } from "@/src/controllers/api/artifactsController"; import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; +import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; @@ -75,6 +77,7 @@ import { setSupportedSyndicateController } from "@/src/controllers/api/setSuppor import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController"; import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController"; import { startDojoRecipeController } from "@/src/controllers/api/startDojoRecipeController"; +import { startLibraryDailyTaskController } from "@/src/controllers/api/startLibraryDailyTaskController"; import { startLibraryPersonalTargetController } from "@/src/controllers/api/startLibraryPersonalTargetController"; import { startRecipeController } from "@/src/controllers/api/startRecipeController"; import { stepSequencersController } from "@/src/controllers/api/stepSequencersController"; @@ -93,7 +96,9 @@ import { upgradesController } from "@/src/controllers/api/upgradesController"; const apiRouter = express.Router(); // get +apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); +apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); apiRouter.get("/credits.php", creditsController); apiRouter.get("/deleteSession.php", deleteSessionController); apiRouter.get("/dojo", dojoController); @@ -121,6 +126,7 @@ apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setBootLocation.php", setBootLocationController); apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController); +apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController); apiRouter.get("/startLibraryPersonalTarget.php", startLibraryPersonalTargetController); apiRouter.get("/surveys.php", surveysController); apiRouter.get("/updateSession.php", updateSessionGetController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 917a3934..5370c74c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -23,7 +23,7 @@ import { IInventoryDatabase, IKubrowPetEggDatabase, IKubrowPetEggClient, - ILibraryAvailableDailyTaskInfo, + ILibraryDailyTaskInfo, ICalendarProgress, IDroneClient, IUpgradeClient @@ -42,6 +42,7 @@ import { ExportBundles, ExportCustoms, ExportDrones, + ExportEnemies, ExportFlavour, ExportFusionBundles, ExportGear, @@ -63,6 +64,8 @@ import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedContro import { addStartingGear } from "@/src/controllers/api/giveStartingGearController"; import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; +import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; +import { getRandomElement, getRandomInt } from "./rngService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -77,7 +80,7 @@ export const createInventory = async ( ReceivedStartingGear: config.skipTutorial }); - inventory.LibraryAvailableDailyTaskInfo = createLibraryAvailableDailyTaskInfo(); + inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.CalendarProgress = createCalendar(); inventory.RewardSeed = generateRewardSeed(); inventory.DuviriInfo = { @@ -1193,15 +1196,19 @@ export const addKeyChainItems = async ( return inventoryChanges; }; -const createLibraryAvailableDailyTaskInfo = (): ILibraryAvailableDailyTaskInfo => { + +export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => { + const enemyTypes = getRandomElement(libraryDailyTasks); + const enemyAvatar = ExportEnemies.avatars[enemyTypes[0]]; + const scansRequired = getRandomInt(2, 4); return { - EnemyTypes: ["/Lotus/Types/Enemies/Orokin/RifleLancerAvatar"], - EnemyLocTag: "/Lotus/Language/Game/CorruptedLancer", - EnemyIcon: "/Lotus/Interface/Icons/Npcs/OrokinRifleLancerAvatar.png", - ScansRequired: 3, - RewardStoreItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/UncommonFusionBundle", - RewardQuantity: 7, - RewardStanding: 7500 + EnemyTypes: enemyTypes, + EnemyLocTag: enemyAvatar.name, + EnemyIcon: enemyAvatar.icon!, + ScansRequired: scansRequired, + RewardStoreItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle", + RewardQuantity: Math.trunc(scansRequired * 2.5), + RewardStanding: 2500 * scansRequired }; }; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index da505c05..774a5db0 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -185,6 +185,22 @@ export const addMissionInventoryUpdates = ( } }); break; + case "LibraryScans": + value.forEach(scan => { + if (inventory.LibraryActiveDailyTaskInfo) { + if (inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType)) { + inventory.LibraryActiveDailyTaskInfo.Scans ??= 0; + inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count; + } else { + logger.warn( + `ignoring synthesis of ${scan.EnemyType} as it's not part of the active daily task` + ); + } + } else { + logger.warn(`no library daily task active, ignoring synthesis of ${scan.EnemyType}`); + } + }); + break; case "SyndicateId": { inventory.CompletedSyndicates.push(value); break; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e93f6b3c..e9acbfc8 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -320,7 +320,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu LibraryPersonalTarget: string; LibraryPersonalProgress: ILibraryPersonalProgress[]; CollectibleSeries: ICollectibleSery[]; - LibraryAvailableDailyTaskInfo: ILibraryAvailableDailyTaskInfo; + LibraryAvailableDailyTaskInfo?: ILibraryDailyTaskInfo; + LibraryActiveDailyTaskInfo?: ILibraryDailyTaskInfo; HasResetAccount: boolean; PendingCoupon?: IPendingCouponClient; Harvestable: boolean; @@ -658,10 +659,11 @@ export interface ILastSortieReward { Manifest: string; } -export interface ILibraryAvailableDailyTaskInfo { +export interface ILibraryDailyTaskInfo { EnemyTypes: string[]; EnemyLocTag: string; EnemyIcon: string; + Scans?: number; ScansRequired: number; RewardStoreItem: string; RewardQuantity: number; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 0220240a..7905dc23 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -92,6 +92,12 @@ export type IMissionInventoryUpdateRequest = { IsFinalWave: boolean; Participants: IVoidTearParticipantInfo[]; }; + LibraryScans?: { + EnemyType: string; + Count: number; + CodexScanCount: number; + Standing: number; + }[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; diff --git a/static/fixed_responses/libraryDailyTasks.json b/static/fixed_responses/libraryDailyTasks.json new file mode 100644 index 00000000..8e6df125 --- /dev/null +++ b/static/fixed_responses/libraryDailyTasks.json @@ -0,0 +1,148 @@ +[ + [ + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserCannonBipedAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/RailgunBipedAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/ShockwaveBipedAvatar" + ], + [ + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar" + ], + [ + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/SuperMoaBipedAvatar" + ], + [ + "/Lotus/Types/Enemies/Corpus/Spaceman/EliteSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/RifleSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/DeployableSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/MeleeSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/SniperSpacemanAvatar" + ], + [ + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BlowtorchSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/PistonSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/BladeSawmanAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EliteRifleLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EviseratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FemaleGrineerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerSniperAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FlameLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/HeavyFemaleGrineerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/HeavyFemaleGrineerAvatarDesert", + "/Lotus/Types/Enemies/Grineer/Forest/HeavyFemaleGrineerAvatarDesert", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerHeavyAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/IncendiaryBombardAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/JetpackMarineAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/MacheteWomanAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerMacheteAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShieldLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/ShotgunLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/GrineerAvatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/GrineerMarinePistolAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/AncientAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/HealingAncientAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/ToxicAncientAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/DiseasedAncientAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/SpawningAncientAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/CrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/NoxiousCrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GraspingCrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GrenadeAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/LightningAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/NaniteCloudBipedAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/SlowBombBipedAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Runners/LeapingRunnerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/OrokinBladeSawmanAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/OrokinNullifySpacemanAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/OrokinRocketBombardAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/RifleLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Orokin/RifleSpacemanAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RocketBombardAvatar" + ] +] \ No newline at end of file From 5ce2e26683e9d22107560cb81840d50c8f6c17d7 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 06:00:40 +0100 Subject: [PATCH 050/776] chore: fix ISlots --- src/services/inventoryService.ts | 5 ++--- src/types/inventoryTypes/inventoryTypes.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 5370c74c..a2071b2c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -668,9 +668,8 @@ export const updateSlots = ( extraAmount: number ): void => { inventory[slotName].Slots += slotAmount; - if (inventory[slotName].Extra === undefined) { - inventory[slotName].Extra = extraAmount; - } else { + if (extraAmount != 0) { + inventory[slotName].Extra ??= 0; inventory[slotName].Extra += extraAmount; } }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e9acbfc8..ed6da027 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -435,7 +435,7 @@ export enum InventorySlot { } export interface ISlots { - Extra: number; // can be undefined, but not if used via mongoose + Extra?: number; Slots: number; } From de794f47ba41ac1e80dc082ebd4bacb8e2654053 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 06:00:54 +0100 Subject: [PATCH 051/776] chore: npm run prettier --- static/fixed_responses/libraryDailyTasks.json | 244 +++++++----------- 1 file changed, 97 insertions(+), 147 deletions(-) diff --git a/static/fixed_responses/libraryDailyTasks.json b/static/fixed_responses/libraryDailyTasks.json index 8e6df125..5e070893 100644 --- a/static/fixed_responses/libraryDailyTasks.json +++ b/static/fixed_responses/libraryDailyTasks.json @@ -1,148 +1,98 @@ [ - [ - "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserCannonBipedAvatar", - "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/RailgunBipedAvatar", - "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/ShockwaveBipedAvatar" - ], - [ - "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar" - ], - [ - "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/SuperMoaBipedAvatar" - ], - [ - "/Lotus/Types/Enemies/Corpus/Spaceman/EliteSpacemanAvatar", - "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/RifleSpacemanAvatar", - "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/DeployableSpacemanAvatar", - "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/MeleeSpacemanAvatar", - "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar", - "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/SniperSpacemanAvatar" - ], - [ - "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BladeSawmanAvatar", - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BlowtorchSawmanAvatar", - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/PistonSawmanAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/BladeSawmanAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/BladeSawmanAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/BladeSawmanAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EliteRifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EliteRifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/RifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EliteRifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/RifleLancerAvatar", - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EliteRifleLancerAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EviseratorLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EvisceratorLancerAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EvisceratorLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FemaleGrineerAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerSniperAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FlameLancerAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/HeavyFemaleGrineerAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/HeavyFemaleGrineerAvatarDesert", - "/Lotus/Types/Enemies/Grineer/Forest/HeavyFemaleGrineerAvatarDesert", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerHeavyAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/IncendiaryBombardAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/JetpackMarineAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/JetpackMarineAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/JetpackMarineAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/JetpackMarineAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/MacheteWomanAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerMacheteAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShieldLancerAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShotgunLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/ShotgunLancerAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/ShotgunLancerAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/ShotgunLancerAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/GrineerAvatars/GrineerMarinePistolAvatar", - "/Lotus/Types/Enemies/Grineer/Desert/Avatars/GrineerMarinePistolAvatar", - "/Lotus/Types/Enemies/Grineer/Forest/Avatars/GrineerMarinePistolAvatar", - "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/GrineerMarinePistolAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/AncientAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/HealingAncientAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/ToxicAncientAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/DiseasedAncientAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/SpawningAncientAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/CrawlerAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/NoxiousCrawlerAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GraspingCrawlerAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GrenadeAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/LightningAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/NaniteCloudBipedAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/SlowBombBipedAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar" - ], - [ - "/Lotus/Types/Enemies/Infested/AiWeek/Runners/LeapingRunnerAvatar", - "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/OrokinBladeSawmanAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/OrokinNullifySpacemanAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/OrokinRocketBombardAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/RifleLancerAvatar" - ], - [ - "/Lotus/Types/Enemies/Orokin/RifleSpacemanAvatar" - ], - [ - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RocketBombardAvatar" - ] -] \ No newline at end of file + [ + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserCannonBipedAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/RailgunBipedAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/ShockwaveBipedAvatar" + ], + ["/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar"], + ["/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/SuperMoaBipedAvatar"], + [ + "/Lotus/Types/Enemies/Corpus/Spaceman/EliteSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/RifleSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/DeployableSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/MeleeSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/SniperSpacemanAvatar" + ], + ["/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BlowtorchSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/PistonSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/BladeSawmanAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EliteRifleLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EviseratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar" + ], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FemaleGrineerAvatar", "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerSniperAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FlameLancerAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/HeavyFemaleGrineerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/HeavyFemaleGrineerAvatarDesert", + "/Lotus/Types/Enemies/Grineer/Forest/HeavyFemaleGrineerAvatarDesert", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerHeavyAvatar" + ], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/IncendiaryBombardAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/JetpackMarineAvatar" + ], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/MacheteWomanAvatar", "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerMacheteAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShieldLancerAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/ShotgunLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/GrineerAvatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/GrineerMarinePistolAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/AncientAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/HealingAncientAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/ToxicAncientAvatar" + ], + ["/Lotus/Types/Enemies/Infested/AiWeek/Ancients/DiseasedAncientAvatar"], + ["/Lotus/Types/Enemies/Infested/AiWeek/Ancients/SpawningAncientAvatar"], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/CrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/NoxiousCrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GraspingCrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GrenadeAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/LightningAvatar" + ], + ["/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/NaniteCloudBipedAvatar", "/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/SlowBombBipedAvatar"], + ["/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar"], + ["/Lotus/Types/Enemies/Infested/AiWeek/Runners/LeapingRunnerAvatar", "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinBladeSawmanAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinNullifySpacemanAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinRocketBombardAvatar"], + ["/Lotus/Types/Enemies/Orokin/RifleLancerAvatar"], + ["/Lotus/Types/Enemies/Orokin/RifleSpacemanAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RocketBombardAvatar"] +] From 9893fa957fd3b0b0ee4016b94d8c0f37cb2eb969 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Feb 2025 21:06:21 -0800 Subject: [PATCH 052/776] fix: purchasing SuitBin slots (#1026) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1026 --- src/controllers/api/inventorySlotsController.ts | 13 ++++++++++--- src/types/requestTypes.ts | 3 --- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/inventorySlotsController.ts b/src/controllers/api/inventorySlotsController.ts index 4caea9d0..d32bc0db 100644 --- a/src/controllers/api/inventorySlotsController.ts +++ b/src/controllers/api/inventorySlotsController.ts @@ -3,6 +3,7 @@ import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { RequestHandler } from "express"; import { updateSlots } from "@/src/services/inventoryService"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; /* loadout slots are additionally purchased slots only @@ -20,14 +21,20 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export const inventorySlotsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - //const body = JSON.parse(req.body as string) as IInventorySlotsRequest; + const body = JSON.parse(req.body as string) as IInventorySlotsRequest; - //TODO: check which slot was purchased because pvpBonus is also possible + if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) { + logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`); + } const inventory = await getInventory(accountId); const currencyChanges = updateCurrency(inventory, 20, true); - updateSlots(inventory, InventorySlot.PVE_LOADOUTS, 1, 1); + updateSlots(inventory, body.Bin, 1, 1); await inventory.save(); res.json({ InventoryChanges: currencyChanges }); }; + +interface IInventorySlotsRequest { + Bin: InventorySlot; +} diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 7905dc23..91408e37 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -121,9 +121,6 @@ export interface IRewardInfo { export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; -export interface IInventorySlotsRequest { - Bin: "PveBonusLoadoutBin"; -} export interface IUpdateGlyphRequest { AvatarImageType: string; AvatarImage: string; From 6a6e3330112688b40600fb76f782487fb74f7432 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 06:15:13 +0100 Subject: [PATCH 053/776] fix: update-translations ignoring translated import_submit entry --- static/webui/translations/en.js | 3 ++- static/webui/translations/ru.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 3251cd63..421acdd4 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -125,5 +125,6 @@ dict = { cheats_quests_resetAll: `Reset All Quests`, cheats_quests_giveAll: `Give All Quests`, import_importNote: `You can provide a full or partial inventory response (client respresentation) here. All fields that are supported by the importer will be overwritten in your account.`, - import_submit: `Submit` + import_submit: `Submit`, + prettier_sucks_ass: `` }; diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 781046b9..b85344c1 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -126,5 +126,6 @@ dict = { cheats_quests_resetAll: `Сбросить прогресс всех квестов`, cheats_quests_giveAll: `Выдать все квесты`, import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля будут перезаписаны в вашем аккаунте.`, - import_submit: `Отправить` + import_submit: `Отправить`, + prettier_sucks_ass: `` }; From e2ee1172ed11a7a1cbdec819edbd66bdcf216772 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 12:16:31 +0100 Subject: [PATCH 054/776] chore: fix most eslint warnings in itemDataService --- src/services/itemDataService.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index c18f3021..42f49517 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -28,6 +28,7 @@ import { ExportSentinels, ExportWarframes, ExportWeapons, + IInboxMessage, IPowersuit, IRecipe, IRegion @@ -149,6 +150,7 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st } const keyChainStage = chainStages[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!keyChainStage) { throw new Error(`KeyChainStage ${ChainStage} not found`); } @@ -163,12 +165,12 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st }; export const getLevelKeyRewards = (levelKey: string) => { - if (!ExportKeys[levelKey]) { + if (!(levelKey in ExportKeys)) { throw new Error(`LevelKey ${levelKey} not found`); } - const levelKeyRewards = ExportKeys[levelKey]?.missionReward; - const levelKeyRewards2 = ExportKeys[levelKey]?.rewards; + const levelKeyRewards = ExportKeys[levelKey].missionReward; + const levelKeyRewards2 = ExportKeys[levelKey].rewards; if (!levelKeyRewards && !levelKeyRewards2) { throw new Error(`LevelKey ${levelKey} does not contain either rewards1 or rewards2`); @@ -182,6 +184,7 @@ export const getLevelKeyRewards = (levelKey: string) => { export const getNode = (nodeName: string): IRegion => { const node = ExportRegions[nodeName]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!node) { throw new Error(`Node ${nodeName} not found`); } @@ -189,7 +192,7 @@ export const getNode = (nodeName: string): IRegion => { return node; }; -export const getQuestCompletionItems = (questKey: string) => { +export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { const items = (questCompletionItems as unknown as Record | undefined)?.[questKey]; if (!items) { @@ -200,13 +203,15 @@ export const getQuestCompletionItems = (questKey: string) => { return items; }; -export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest) => { +export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IInboxMessage => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[KeyChain]?.chainStages; if (!chainStages) { throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); } const keyChainStage = chainStages[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!keyChainStage) { throw new Error(`KeyChainStage ${ChainStage} not found`); } From 4471b2d64d75421f6058dbd62548ac9336848e75 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 26 Feb 2025 15:15:32 -0800 Subject: [PATCH 055/776] fix(webui): typo (#1035) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1035 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/en.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 421acdd4..750dfc94 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -102,7 +102,7 @@ dict = { cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`, cheats_unlockAllShipFeatures: `Unlock All Ship Features`, cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`, - cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, + cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, cheats_unlockAllSkins: `Unlock All Skins`, cheats_unlockAllCapturaScenes: `Unlock All Captura Scenes`, cheats_universalPolarityEverywhere: `Universal Polarity Everywhere`, From a5c45bb646ab6118acd2f8f29d3986e73fb978ea Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 15:41:07 -0800 Subject: [PATCH 056/776] fix: consume a slot when item is crafted instead of bought via plat (#1029) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1029 --- .../api/claimCompletedRecipeController.ts | 2 +- src/controllers/api/guildTechController.ts | 6 +- src/controllers/custom/addItemsController.ts | 2 +- src/services/inventoryService.ts | 93 ++++++++++--------- src/services/purchaseService.ts | 24 +++-- src/types/purchaseTypes.ts | 64 ++++++++----- 6 files changed, 103 insertions(+), 88 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index ec9087ba..d8208f7f 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -91,7 +91,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } InventoryChanges = { ...InventoryChanges, - ...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges + ...(await addItem(inventory, recipe.resultType, recipe.num, false)).InventoryChanges }; await inventory.save(); res.json({ InventoryChanges }); diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 34022699..b9d3b75d 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -54,10 +54,8 @@ export const guildTechController: RequestHandler = async (req, res) => { } } addMiscItems(inventory, miscItemChanges); - const inventoryChanges: IInventoryChanges = { - ...updateCurrency(inventory, contributions.RegularCredits, false), - MiscItems: miscItemChanges - }; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); + inventoryChanges.MiscItems = miscItemChanges; if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { // This research is now fully funded. diff --git a/src/controllers/custom/addItemsController.ts b/src/controllers/custom/addItemsController.ts index 1eb50ed6..15837602 100644 --- a/src/controllers/custom/addItemsController.ts +++ b/src/controllers/custom/addItemsController.ts @@ -7,7 +7,7 @@ export const addItemsController: RequestHandler = async (req, res) => { const requests = req.body as IAddItemRequest[]; const inventory = await getInventory(accountId); for (const request of requests) { - await addItem(inventory, request.ItemType, request.ItemCount); + await addItem(inventory, request.ItemType, request.ItemCount, true); } await inventory.save(); res.end(); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index a2071b2c..f4e48ca8 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -5,7 +5,7 @@ import { } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; import { HydratedDocument, Types } from "mongoose"; -import { SlotNames, IInventoryChanges, IBinChanges, ICurrencyChanges } from "@/src/types/purchaseTypes"; +import { SlotNames, IInventoryChanges, IBinChanges, slotNames } from "@/src/types/purchaseTypes"; import { IChallengeProgress, IConsumable, @@ -126,13 +126,17 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del for (const item of right) { left.push(item); } - } else if (typeof delta[key] == "object") { - console.assert(key.substring(-3) == "Bin"); - console.assert(key != "InfestedFoundry"); - const left = InventoryChanges[key] as IBinChanges; - const right = delta[key] as IBinChanges; - left.count += right.count; - left.platinum += right.platinum; + } else if (slotNames.indexOf(key as SlotNames) != -1) { + const left = InventoryChanges[key as SlotNames]!; + const right = delta[key as SlotNames]!; + if (right.count) { + left.count ??= 0; + left.count += right.count; + } + if (right.platinum) { + left.platinum ??= 0; + left.platinum += right.platinum; + } left.Slots += right.Slots; if (right.Extra) { left.Extra ??= 0; @@ -159,10 +163,32 @@ export const getInventory = async ( return inventory; }; +const occupySlot = ( + inventory: TInventoryDatabaseDocument, + bin: InventorySlot, + premiumPurchase: boolean +): IInventoryChanges => { + const slotChanges = { + Slots: 0, + Extra: 0 + }; + if (premiumPurchase) { + slotChanges.Extra += 1; + } else { + // { count: 1, platinum: 0, Slots: -1 } + slotChanges.Slots -= 1; + } + updateSlots(inventory, bin, slotChanges.Slots, slotChanges.Extra); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[bin] = slotChanges satisfies IBinChanges; + return inventoryChanges; +}; + export const addItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, - quantity: number = 1 + quantity: number = 1, + premiumPurchase: boolean = false ): Promise<{ InventoryChanges: IInventoryChanges }> => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { @@ -302,14 +328,13 @@ export const addItem = async ( const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { - combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + combineInventoryChanges(inventoryChanges, (await addItem(inventory, item, 1)).InventoryChanges); } } - updateSlots(inventory, InventorySlot.WEAPONS, 0, 1); return { InventoryChanges: { ...inventoryChanges, - WeaponBin: { count: 1, platinum: 0, Slots: -1 } + ...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase) } }; } else { @@ -378,44 +403,26 @@ export const addItem = async ( case "Powersuits": switch (typeName.substr(1).split("/")[2]) { default: { - const inventoryChanges = addPowerSuit(inventory, typeName); - updateSlots(inventory, InventorySlot.SUITS, 0, 1); return { InventoryChanges: { - ...inventoryChanges, - SuitBin: { - count: 1, - platinum: 0, - Slots: -1 - } + ...addPowerSuit(inventory, typeName), + ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) } }; } case "Archwing": { - const inventoryChanges = addSpaceSuit(inventory, typeName); - updateSlots(inventory, InventorySlot.SPACESUITS, 0, 1); return { InventoryChanges: { - ...inventoryChanges, - SpaceSuitBin: { - count: 1, - platinum: 0, - Slots: -1 - } + ...addSpaceSuit(inventory, typeName), + ...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase) } }; } case "EntratiMech": { - const inventoryChanges = addMechSuit(inventory, typeName); - updateSlots(inventory, InventorySlot.MECHSUITS, 0, 1); return { InventoryChanges: { - ...inventoryChanges, - MechBin: { - count: 1, - platinum: 0, - Slots: -1 - } + ...addMechSuit(inventory, typeName), + ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) } }; } @@ -446,12 +453,10 @@ export const addItem = async ( case "Types": switch (typeName.substr(1).split("/")[2]) { case "Sentinels": { - const inventoryChanges = addSentinel(inventory, typeName); - updateSlots(inventory, InventorySlot.SENTINELS, 0, 1); return { InventoryChanges: { - ...inventoryChanges, - SentinelBin: { count: 1, platinum: 0, Slots: -1 } + ...addSentinel(inventory, typeName), + ...occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase) } }; } @@ -531,9 +536,9 @@ export const addItems = async ( let inventoryDelta; for (const item of items) { if (typeof item === "string") { - inventoryDelta = await addItem(inventory, item); + inventoryDelta = await addItem(inventory, item, 1, true); } else { - inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount); + inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount, true); } combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); } @@ -682,8 +687,8 @@ export const updateCurrency = ( inventory: TInventoryDatabaseDocument, price: number, usePremium: boolean -): ICurrencyChanges => { - const currencyChanges: ICurrencyChanges = {}; +): IInventoryChanges => { + const currencyChanges: IInventoryChanges = {}; if (price != 0 && isCurrencyTracked(usePremium)) { if (usePremium) { if (inventory.PremiumCreditsFree > 0) { diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 76a6d111..2d1c66d3 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -164,7 +164,7 @@ export const handlePurchase = async ( addMiscItems(inventory, [invItem]); purchaseResponse.InventoryChanges.MiscItems ??= []; - (purchaseResponse.InventoryChanges.MiscItems as IMiscItem[]).push(invItem); + purchaseResponse.InventoryChanges.MiscItems.push(invItem); } else if (!config.infiniteRegalAya) { inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; } @@ -191,11 +191,11 @@ const handleItemPrices = ( addMiscItems(inventory, [invItem]); inventoryChanges.MiscItems ??= []; - const change = (inventoryChanges.MiscItems as IMiscItem[]).find(x => x.ItemType == item.ItemType); + const change = inventoryChanges.MiscItems.find(x => x.ItemType == item.ItemType); if (change) { change.ItemCount += invItem.ItemCount; } else { - (inventoryChanges.MiscItems as IMiscItem[]).push(invItem); + inventoryChanges.MiscItems.push(invItem); } } }; @@ -251,7 +251,7 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = await addItem(inventory, internalName, quantity); + purchaseResponse = await addItem(inventory, internalName, quantity, true); break; } case "Types": @@ -300,16 +300,14 @@ const handleSlotPurchase = ( logger.debug(`added ${slotsPurchased} slot ${slotName}`); - return { - InventoryChanges: { - [slotName]: { - count: 0, - platinum: 1, - Slots: slotsPurchased, - Extra: slotsPurchased - } - } + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[slotName] = { + count: 0, + platinum: 1, + Slots: slotsPurchased, + Extra: slotsPurchased }; + return { InventoryChanges: inventoryChanges }; }; const handleBoosterPackPurchase = async ( diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index f1e864be..e921d136 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -1,5 +1,5 @@ import { IEquipmentClient } from "./inventoryTypes/commonInventoryTypes"; -import { IDroneClient, IInfestedFoundryClient, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; +import { IDroneClient, IInfestedFoundryClient, IMiscItem, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { PurchaseParams: IPurchaseParams; @@ -22,20 +22,31 @@ export interface IPurchaseParams { IsWeekly?: boolean; // for Source 7 } -export interface ICurrencyChanges { - RegularCredits?: number; - PremiumCredits?: number; - PremiumCreditsFree?: number; -} - export type IInventoryChanges = { [_ in SlotNames]?: IBinChanges; } & { [_ in TEquipmentKey]?: IEquipmentClient[]; -} & ICurrencyChanges & { - InfestedFoundry?: IInfestedFoundryClient; - Drones?: IDroneClient[]; - } & Record; +} & { + RegularCredits?: number; + PremiumCredits?: number; + PremiumCreditsFree?: number; + InfestedFoundry?: IInfestedFoundryClient; + Drones?: IDroneClient[]; + MiscItems?: IMiscItem[]; +} & Record< + Exclude< + string, + | SlotNames + | TEquipmentKey + | "RegularCredits" + | "PremiumCredits" + | "PremiumCreditsFree" + | "InfestedFoundry" + | "Drones" + | "MiscItems" + >, + number | object[] + >; export interface IAffiliationMods { Tag: string; @@ -51,8 +62,8 @@ export interface IPurchaseResponse { } export type IBinChanges = { - count: number; - platinum: number; + count?: number; + platinum?: number; Slots: number; Extra?: number; }; @@ -69,18 +80,21 @@ export type SlotPurchaseName = | "TwoCrewShipSalvageSlotItem" | "CrewMemberSlotItem"; -export type SlotNames = - | "SuitBin" - | "WeaponBin" - | "MechBin" - | "PveBonusLoadoutBin" - | "SentinelBin" - | "SpaceSuitBin" - | "SpaceWeaponBin" - | "OperatorAmpBin" - | "RandomModBin" - | "CrewShipSalvageBin" - | "CrewMemberBin"; +export const slotNames = [ + "SuitBin", + "WeaponBin", + "MechBin", + "PveBonusLoadoutBin", + "SentinelBin", + "SpaceSuitBin", + "SpaceWeaponBin", + "OperatorAmpBin", + "RandomModBin", + "CrewShipSalvageBin", + "CrewMemberBin" +] as const; + +export type SlotNames = (typeof slotNames)[number]; export type SlotPurchase = { [P in SlotPurchaseName]: { name: SlotNames; slotsPerPurchase: number }; From 58ec63f7b99cde809afde3f66884e7691f5f835d Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 15:41:36 -0800 Subject: [PATCH 057/776] chore: update mongoose (#1036) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1036 --- package-lock.json | 54 ++++++++++++++++++----------------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72f2bd61..9f7bfbea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "copyfiles": "^2.4.1", "express": "^5", - "mongoose": "^8.9.4", + "mongoose": "^8.11.0", "warframe-public-export-plus": "^0.5.37", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", @@ -247,10 +247,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", "dependencies": { "sparse-bitfield": "^3.0.3" } @@ -475,14 +474,12 @@ "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", "dependencies": { "@types/webidl-conversions": "*" } @@ -969,10 +966,9 @@ } }, "node_modules/bson": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz", - "integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==", - "license": "Apache-2.0", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", "engines": { "node": ">=16.20.1" } @@ -2543,8 +2539,7 @@ "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, "node_modules/merge-descriptors": { "version": "2.0.0", @@ -2660,20 +2655,19 @@ } }, "node_modules/mongodb": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", - "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", - "license": "Apache-2.0", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.1.tgz", + "integrity": "sha512-gdq40tX8StmhP6akMp1pPoEVv+9jTYFSrga/g23JxajPAQhH39ysZrHGzQCSd9PEOnuEQEdjIWqxO7ZSwC0w7Q==", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", + "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "engines": { "node": ">=16.20.1" }, "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", + "@aws-sdk/credential-providers": "^3.632.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", @@ -2709,21 +2703,19 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz", - "integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==", - "license": "MIT", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.11.0.tgz", + "integrity": "sha512-xaQSuaLk2JKmXI5zDVVWXVCQTnWhAe8MFOijMnwOuP/wucKVphd3f+ouDKivCDMGjYBDrR7dtoyV0U093xbKqA==", "dependencies": { "bson": "^6.10.1", "kareem": "2.6.3", - "mongodb": "~6.12.0", + "mongodb": "~6.13.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -3589,7 +3581,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" } @@ -3795,7 +3786,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, @@ -4106,16 +4096,14 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/whatwg-url": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", - "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", - "license": "MIT", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" diff --git a/package.json b/package.json index 5bbcc52b..e6e1002b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "copyfiles": "^2.4.1", "express": "^5", - "mongoose": "^8.9.4", + "mongoose": "^8.11.0", "warframe-public-export-plus": "^0.5.37", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", From 08f4137d712cf264d1bdc3cb3902563cbe77551d Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 15:42:13 -0800 Subject: [PATCH 058/776] fix: propagate relic reward's itemCount (#1030) Preemptive fix for a visual bug after completing a non-endless fissure. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1030 --- src/helpers/relicHelper.ts | 6 ++++-- src/services/missionInventoryUpdateService.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts index 6e28aef0..a78c99ec 100644 --- a/src/helpers/relicHelper.ts +++ b/src/helpers/relicHelper.ts @@ -1,7 +1,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; -import { getRandomWeightedReward2 } from "@/src/services/rngService"; +import { getRandomWeightedReward2, IRngResult } from "@/src/services/rngService"; import { logger } from "@/src/utils/logger"; import { addMiscItems } from "@/src/services/inventoryService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; @@ -9,7 +9,7 @@ import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; export const crackRelic = async ( inventory: TInventoryDatabaseDocument, participant: IVoidTearParticipantInfo -): Promise => { +): Promise => { const relic = ExportRelics[participant.VoidProjection]; const weights = refinementToWeights[relic.quality]; logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); @@ -30,6 +30,8 @@ export const crackRelic = async ( // Give reward await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount); + + return reward; }; const refinementToWeights = { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 774a5db0..87cf5ae6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -320,8 +320,8 @@ export const addMissionRewards = async ( voidTearWave.Participants[0].QualifiesForReward && !voidTearWave.Participants[0].HaveRewardResponse ) { - await crackRelic(inventory, voidTearWave.Participants[0]); - MissionRewards.push({ StoreItem: voidTearWave.Participants[0].Reward, ItemCount: 1 }); + const reward = await crackRelic(inventory, voidTearWave.Participants[0]); + MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount }); } return { inventoryChanges, MissionRewards, credits }; From ca55b21a2a255b3f58d4ac73287e6b82a5d608e6 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Feb 2025 15:42:25 -0800 Subject: [PATCH 059/776] fix: display bug when activating riven via mods console (#1034) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1034 --- src/controllers/api/activateRandomModController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/activateRandomModController.ts b/src/controllers/api/activateRandomModController.ts index cb84feba..02feb87e 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -1,3 +1,4 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { addMods, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -47,8 +48,13 @@ export const activateRandomModController: RequestHandler = async (req, res) => { UpgradeFingerprint: JSON.stringify({ challenge: fingerprintChallenge }) }) - 1; await inventory.save(); + // For some reason, in this response, the UpgradeFingerprint is simply a nested object and not a string res.json({ - NewMod: inventory.Upgrades[upgradeIndex].toJSON() + NewMod: { + UpgradeFingerprint: { challenge: fingerprintChallenge }, + ItemType: inventory.Upgrades[upgradeIndex].ItemType, + ItemId: toOid(inventory.Upgrades[upgradeIndex]._id) + } }); }; From 550ad360d73b6cdd726faf25548d406c9e2613da Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Feb 2025 09:58:45 +0100 Subject: [PATCH 060/776] Revert "remove .coderabbit.yaml" This reverts commit 0f250c61033ba76615ddf28b67224ed75dc1876e. --- .coderabbit.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..29d9043a --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + changed_files_summary: false + high_level_summary: false + poem: false + review_status: true + commit_status: false + collapse_walkthrough: false + sequence_diagrams: false + related_prs: false + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true From fac3d2f901ea1695572c9222c5c83233a83849e9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Feb 2025 10:01:00 +0100 Subject: [PATCH 061/776] fix: only run docker workflow on the main repository --- .github/workflows/docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 750a593c..55626376 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,6 +5,7 @@ on: - main jobs: docker: + if: github.repository == 'OpenWF/SpaceNinjaServer' runs-on: ubuntu-latest steps: - name: Set up Docker buildx From a8c7eebf6d0a48083484c2a080be8bbb4182dfed Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Feb 2025 01:12:32 -0800 Subject: [PATCH 062/776] chore: note for mirror lookers --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4dcab407..1f68f700 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY) +>[!NOTE] +>Development of this project currently happens on . If that's not the site you're on, you're looking at a one-way mirror. + ## config.json - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. From 9267c9929e9fde3df0953e9beaacb004af16acfa Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Feb 2025 18:00:37 -0800 Subject: [PATCH 063/776] feat(webui): acquire flawed mods & imposters via add mods (#1040) Closes #1033 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1040 --- .../custom/getItemListsController.ts | 23 +++++++++++-------- static/webui/script.js | 13 +++++++---- static/webui/translations/en.js | 1 + static/webui/translations/ru.js | 1 + 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 86488d12..ce80d041 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -20,6 +20,7 @@ interface ListedItem { name: string; fusionLimit?: number; exalted?: string[]; + badReason?: "starter" | "frivolous" | "notraw"; } const getItemListsController: RequestHandler = (req, response) => { @@ -132,16 +133,20 @@ const getItemListsController: RequestHandler = (req, response) => { } res.mods = []; - const badItems: Record = {}; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { - res.mods.push({ + const mod: ListedItem = { uniqueName, name: getString(upgrade.name, lang), fusionLimit: upgrade.fusionLimit - }); - if (upgrade.isStarter || upgrade.isFrivolous || upgrade.upgradeEntries) { - badItems[uniqueName] = true; + }; + if (upgrade.isStarter) { + mod.badReason = "starter"; + } else if (upgrade.isFrivolous) { + mod.badReason = "frivolous"; + } else if (upgrade.upgradeEntries) { + mod.badReason = "notraw"; } + res.mods.push(mod); } for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) { res.mods.push({ @@ -151,13 +156,14 @@ const getItemListsController: RequestHandler = (req, response) => { }); } for (const [uniqueName, arcane] of Object.entries(ExportArcanes)) { - res.mods.push({ + const mod: ListedItem = { uniqueName, name: getString(arcane.name, lang) - }); + }; if (arcane.isFrivolous) { - badItems[uniqueName] = true; + mod.badReason = "frivolous"; } + res.mods.push(mod); } for (const [uniqueName, syndicate] of Object.entries(ExportSyndicates)) { res.Syndicates.push({ @@ -167,7 +173,6 @@ const getItemListsController: RequestHandler = (req, response) => { } response.json({ - badItems, archonCrystalUpgrades, uniqueLevelCaps: ExportMisc.uniqueLevelCaps, ...res diff --git a/static/webui/script.js b/static/webui/script.js index 1c55cf4d..a73097db 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -207,11 +207,16 @@ function fetchItemList() { document.getElementById("changeSyndicate").appendChild(option); itemMap[item.uniqueName] = { ...item, type }; }); - } else if (type != "badItems") { + } else { items.forEach(item => { - if (item.uniqueName in data.badItems) { - item.name += " " + loc("code_badItem"); - } else if (item.uniqueName.substr(0, 18) != "/Lotus/Types/Game/") { + if ("badReason" in item) { + if (item.badReason == "starter") { + item.name = loc("code_starter").split("|MOD|").join(item.name); + } else { + item.name += " " + loc("code_badItem"); + } + } + if (item.uniqueName.substr(0, 18) != "/Lotus/Types/Game/" && item.badReason != "notraw") { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 750dfc94..f69a8fd3 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -18,6 +18,7 @@ dict = { code_kDrive: `K-Drive`, code_legendaryCore: `Legendary Core`, code_traumaticPeculiar: `Traumatic Peculiar`, + code_starter: `|MOD| (Flawed)`, code_badItem: `(Imposter)`, code_maxRank: `Max Rank`, code_rename: `Rename`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index b85344c1..d2cc9a02 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -19,6 +19,7 @@ dict = { code_kDrive: `К-Драйв`, code_legendaryCore: `Легендарное ядро`, code_traumaticPeculiar: `Травмирующая Странность`, + code_starter: `[UNTRANSLATED] |MOD| (Flawed)`, code_badItem: `(Самозванец)`, code_maxRank: `Максимальный ранг`, code_rename: `Переименовать`, From 526ce1529b20d9da812e66bf38ba98035e7f1ca1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Feb 2025 18:01:06 -0800 Subject: [PATCH 064/776] feat: unveiling rivens by doing the challenge (#1031) Closes #722 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1031 --- .../api/activateRandomModController.ts | 10 +-- .../completeRandomModChallengeController.ts | 15 +---- .../api/rerollRandomModController.ts | 67 ++++++++++++------- src/helpers/rivenFingerprintHelper.ts | 29 +++++++- src/services/missionInventoryUpdateService.ts | 6 ++ src/types/requestTypes.ts | 4 +- 6 files changed, 85 insertions(+), 46 deletions(-) diff --git a/src/controllers/api/activateRandomModController.ts b/src/controllers/api/activateRandomModController.ts index 02feb87e..1774e4f7 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -1,4 +1,5 @@ import { toOid } from "@/src/helpers/inventoryHelpers"; +import { IRivenChallenge } from "@/src/helpers/rivenFingerprintHelper"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { addMods, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -19,7 +20,7 @@ export const activateRandomModController: RequestHandler = async (req, res) => { ]); const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]); const challenge = getRandomElement(ExportUpgrades[rivenType].availableChallenges!); - const fingerprintChallenge: IRandomModChallenge = { + const fingerprintChallenge: IRivenChallenge = { Type: challenge.fullName, Progress: 0, Required: getRandomInt(challenge.countRange[0], challenge.countRange[1]) @@ -62,13 +63,6 @@ interface IActiveRandomModRequest { ItemType: string; } -interface IRandomModChallenge { - Type: string; - Progress: number; - Required: number; - Complication?: string; -} - const rivenRawToRealWeighted: Record = { "/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [ "/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare" diff --git a/src/controllers/api/completeRandomModChallengeController.ts b/src/controllers/api/completeRandomModChallengeController.ts index c2578eb7..ef5e7d2a 100644 --- a/src/controllers/api/completeRandomModChallengeController.ts +++ b/src/controllers/api/completeRandomModChallengeController.ts @@ -4,8 +4,7 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; -import { getRandomElement, getRandomInt } from "@/src/services/rngService"; +import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenFingerprintHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; export const completeRandomModChallengeController: RequestHandler = async (req, res) => { @@ -31,17 +30,7 @@ export const completeRandomModChallengeController: RequestHandler = async (req, // Update riven fingerprint to a randomised unveiled state const upgrade = inventory.Upgrades.id(request.ItemId)!; const meta = ExportUpgrades[upgrade.ItemType]; - const fingerprint: IUnveiledRivenFingerprint = { - compat: getRandomElement(meta.compatibleItems!), - lim: 0, - lvl: 0, - lvlReq: getRandomInt(8, 16), - pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), - buffs: [], - curses: [] - }; - randomiseRivenStats(meta, fingerprint); - upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta)); await inventory.save(); diff --git a/src/controllers/api/rerollRandomModController.ts b/src/controllers/api/rerollRandomModController.ts index e35df1a8..9dc84e5f 100644 --- a/src/controllers/api/rerollRandomModController.ts +++ b/src/controllers/api/rerollRandomModController.ts @@ -2,43 +2,58 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; +import { + createUnveiledRivenFingerprint, + randomiseRivenStats, + RivenFingerprint +} from "@/src/helpers/rivenFingerprintHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; +import { IOid } from "@/src/types/commonTypes"; export const rerollRandomModController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const request = getJSONfromString(String(req.body)); if ("ItemIds" in request) { const inventory = await getInventory(accountId, "Upgrades MiscItems"); - const upgrade = inventory.Upgrades.id(request.ItemIds[0])!; - const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as IUnveiledRivenFingerprint; + const changes: IChange[] = []; + let totalKuvaCost = 0; + request.ItemIds.forEach(itemId => { + const upgrade = inventory.Upgrades.id(itemId)!; + const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as RivenFingerprint; + if ("challenge" in fingerprint) { + upgrade.UpgradeFingerprint = JSON.stringify( + createUnveiledRivenFingerprint(ExportUpgrades[upgrade.ItemType]) + ); + } else { + fingerprint.rerolls ??= 0; + const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500; + totalKuvaCost += kuvaCost; + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Items/MiscItems/Kuva", + ItemCount: kuvaCost * -1 + } + ]); - fingerprint.rerolls ??= 0; - const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500; - addMiscItems(inventory, [ - { - ItemType: "/Lotus/Types/Items/MiscItems/Kuva", - ItemCount: kuvaCost * -1 + fingerprint.rerolls++; + upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + + randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); + upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); } - ]); - fingerprint.rerolls++; - upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); - - randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); - upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); + changes.push({ + ItemId: { $oid: request.ItemIds[0] }, + UpgradeFingerprint: upgrade.UpgradeFingerprint, + PendingRerollFingerprint: upgrade.PendingRerollFingerprint + }); + }); await inventory.save(); res.json({ - changes: [ - { - ItemId: { $oid: request.ItemIds[0] }, - UpgradeFingerprint: upgrade.UpgradeFingerprint, - PendingRerollFingerprint: upgrade.PendingRerollFingerprint - } - ], - cost: kuvaCost + changes: changes, + cost: totalKuvaCost }); } else { const inventory = await getInventory(accountId, "Upgrades"); @@ -63,4 +78,10 @@ interface AwDangitRequest { CommitReroll: boolean; } +interface IChange { + ItemId: IOid; + UpgradeFingerprint?: string; + PendingRerollFingerprint?: string; +} + const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150]; diff --git a/src/helpers/rivenFingerprintHelper.ts b/src/helpers/rivenFingerprintHelper.ts index c3742391..e5fa261d 100644 --- a/src/helpers/rivenFingerprintHelper.ts +++ b/src/helpers/rivenFingerprintHelper.ts @@ -1,5 +1,18 @@ import { IUpgrade } from "warframe-public-export-plus"; -import { getRandomElement } from "../services/rngService"; +import { getRandomElement, getRandomInt } from "../services/rngService"; + +export type RivenFingerprint = IVeiledRivenFingerprint | IUnveiledRivenFingerprint; + +export interface IVeiledRivenFingerprint { + challenge: IRivenChallenge; +} + +export interface IRivenChallenge { + Type: string; + Progress: number; + Required: number; + Complication?: string; +} export interface IUnveiledRivenFingerprint { compat: string; @@ -17,6 +30,20 @@ interface IRivenStat { Value: number; } +export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => { + const fingerprint: IUnveiledRivenFingerprint = { + compat: getRandomElement(meta.compatibleItems!), + lim: 0, + lvl: 0, + lvlReq: getRandomInt(8, 16), + pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), + buffs: [], + curses: [] + }; + randomiseRivenStats(meta, fingerprint); + return fingerprint; +}; + export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenFingerprint): void => { fingerprint.buffs = []; const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 87cf5ae6..60767ea1 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -201,6 +201,12 @@ export const addMissionInventoryUpdates = ( } }); break; + case "Upgrades": + value.forEach(clientUpgrade => { + const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!; + upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress + }); + break; case "SyndicateId": { inventory.CompletedSyndicates.push(value); break; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 91408e37..9ebf3adb 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -14,7 +14,8 @@ import { ICustomMarkers, IPlayerSkills, IQuestKeyDatabase, - ILoreFragmentScan + ILoreFragmentScan, + IUpgradeClient } from "./inventoryTypes/inventoryTypes"; export interface IThemeUpdateRequest { @@ -98,6 +99,7 @@ export type IMissionInventoryUpdateRequest = { CodexScanCount: number; Standing: number; }[]; + Upgrades?: IUpgradeClient[]; // riven challenge progress } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From d63bab1bf43265ab20a75b612140c04d3434041b Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 03:51:08 +0100 Subject: [PATCH 065/776] fix: logic error in addCrewShipHarness --- src/services/inventoryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index f4e48ca8..df6271de 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -855,7 +855,7 @@ const addCrewShipHarness = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - if (inventory.CrewShips.length != 0) { + if (inventory.CrewShipHarnesses.length != 0) { throw new Error("refusing to add CrewShipHarness because account already has one"); } const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; From 08a4dba80bf48a30d6028899958c5d04026acce0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Feb 2025 21:54:31 -0800 Subject: [PATCH 066/776] fix: put ayatan statues in FusionTreasures instead of MiscItems (#1046) Fixes #1044 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1046 --- src/services/inventoryService.ts | 34 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index df6271de..7df65891 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -224,6 +224,20 @@ export const addItem = async ( MiscItems: miscItemChanges } }; + } else if (ExportResources[typeName].productCategory == "FusionTreasures") { + const fusionTreasureChanges = [ + { + ItemType: typeName, + ItemCount: quantity, + Sockets: 0 + } satisfies IFusionTreasure + ]; + addFusionTreasures(inventory, fusionTreasureChanges); + return { + InventoryChanges: { + FusionTreasures: fusionTreasureChanges + } + }; } else if (ExportResources[typeName].productCategory == "Ships") { const oid = await createShip(inventory.accountOwnerId, typeName); inventory.Ships.push(oid); @@ -281,6 +295,8 @@ export const addItem = async ( KubrowPetEggs: changes } }; + } else { + throw new Error(`unknown product category: ${ExportResources[typeName].productCategory}`); } } if (typeName in ExportCustoms) { @@ -460,24 +476,6 @@ export const addItem = async ( } }; } - case "Items": { - switch (typeName.substr(1).split("/")[3]) { - default: { - const miscItemChanges = [ - { - ItemType: typeName, - ItemCount: quantity - } satisfies IMiscItem - ]; - addMiscItems(inventory, miscItemChanges); - return { - InventoryChanges: { - MiscItems: miscItemChanges - } - }; - } - } - } case "Game": { if (typeName.substr(1).split("/")[3] == "Projections") { // Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze From 05c0a91f821958a1063b67212c4653701bbaf457 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 07:08:33 +0100 Subject: [PATCH 067/776] chore: make this reference more future proof --- src/services/missionInventoryUpdateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 60767ea1..d52b6b7f 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -290,7 +290,7 @@ export const addMissionRewards = async ( if ( missions && - missions.Tag != "" // #1013 + missions.Tag != "" // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013 ) { const node = getNode(missions.Tag); From 28b9e35d8d544e4ba7aa13e34f8f8fd35ac42189 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 12:04:43 +0100 Subject: [PATCH 068/776] chore: remove string[] from combineInventoryChanges --- src/services/inventoryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 7df65891..93497d29 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -122,7 +122,7 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del InventoryChanges[key] = delta[key]; } else if (Array.isArray(delta[key])) { const left = InventoryChanges[key] as object[]; - const right: object[] | string[] = delta[key]; + const right: object[] = delta[key]; for (const item of right) { left.push(item); } From 1468c6b1d2db2bfa1fc87305de2babef8accc163 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 03:04:59 -0800 Subject: [PATCH 069/776] chore: update PE+ to 0.5.38 (#1048) Fixess #1041 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1048 --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/itemDataService.ts | 8 ++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f7bfbea..8f67ed87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.37", + "warframe-public-export-plus": "^0.5.38", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4083,9 +4083,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.37", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.37.tgz", - "integrity": "sha512-atpTQ0IV0HF17rO2+Z+Vdv8nnnUxh5HhkcXBjc5iwY8tlvRgRWwHemq4PdA1bxR4tYFU5xoYjlgDe5D8ZfM4ew==" + "version": "0.5.38", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.38.tgz", + "integrity": "sha512-yvc86eOmYPSnnU8LzLBhg/lR1AS1RHID24TqFHVcZuOzMYc934NL8Cv7rtllyefWAMyl7iA5x9tyXSuJWbi6CA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index e6e1002b..f7a70d38 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.37", + "warframe-public-export-plus": "^0.5.38", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 42f49517..f24f5032 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -29,9 +29,11 @@ import { ExportWarframes, ExportWeapons, IInboxMessage, + IMissionReward, IPowersuit, IRecipe, - IRegion + IRegion, + TReward } from "warframe-public-export-plus"; import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; @@ -164,7 +166,9 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st return keyChainStage.itemsToGiveWhenTriggered; }; -export const getLevelKeyRewards = (levelKey: string) => { +export const getLevelKeyRewards = ( + levelKey: string +): { levelKeyRewards?: IMissionReward; levelKeyRewards2?: TReward[] } => { if (!(levelKey in ExportKeys)) { throw new Error(`LevelKey ${levelKey} not found`); } From cfaafc2cc3bc85efb2218dd52820c09cc5746238 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 03:05:32 -0800 Subject: [PATCH 070/776] chore: remove undefined as a possible argument when committing inventory change (#1047) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1047 --- src/services/inventoryService.ts | 61 +++++++++++++------------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 93497d29..d4a408ff 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -890,12 +890,12 @@ const addDrone = ( //TODO: wrong id is not erroring export const addGearExpByCategory = ( inventory: TInventoryDatabaseDocument, - gearArray: IEquipmentClient[] | undefined, + gearArray: IEquipmentClient[], categoryName: TEquipmentKey ): void => { const category = inventory[categoryName]; - gearArray?.forEach(({ ItemId, XP }) => { + gearArray.forEach(({ ItemId, XP }) => { if (!XP) { return; } @@ -919,10 +919,10 @@ export const addGearExpByCategory = ( }); }; -export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[] | undefined): void => { +export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => { const { MiscItems } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { if (ItemCount == 0) { return; } @@ -941,13 +941,10 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: }); }; -export const addShipDecorations = ( - inventory: TInventoryDatabaseDocument, - itemsArray: IConsumable[] | undefined -): void => { +export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[]): void => { const { ShipDecorations } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { const itemIndex = ShipDecorations.findIndex(miscItem => miscItem.ItemType === ItemType); if (itemIndex !== -1) { @@ -958,10 +955,10 @@ export const addShipDecorations = ( }); }; -export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[] | undefined): void => { +export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[]): void => { const { Consumables } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { const itemIndex = Consumables.findIndex(i => i.ItemType === ItemType); if (itemIndex !== -1) { @@ -972,13 +969,10 @@ export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray }); }; -export const addCrewShipRawSalvage = ( - inventory: TInventoryDatabaseDocument, - itemsArray: ITypeCount[] | undefined -): void => { +export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { const { CrewShipRawSalvage } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { const itemIndex = CrewShipRawSalvage.findIndex(i => i.ItemType === ItemType); if (itemIndex !== -1) { @@ -989,10 +983,10 @@ export const addCrewShipRawSalvage = ( }); }; -export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[] | undefined): void => { +export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { const { CrewShipAmmo } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { const itemIndex = CrewShipAmmo.findIndex(i => i.ItemType === ItemType); if (itemIndex !== -1) { @@ -1003,10 +997,10 @@ export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArra }); }; -export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[] | undefined): void => { +export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { const { Recipes } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { const itemIndex = Recipes.findIndex(i => i.ItemType === ItemType); if (itemIndex !== -1) { @@ -1017,10 +1011,10 @@ export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: IT }); }; -export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[] | undefined): void => { +export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => { const { RawUpgrades } = inventory; - itemsArray?.forEach(({ ItemType, ItemCount }) => { + itemsArray.forEach(({ ItemType, ItemCount }) => { if (ItemCount == 0) { return; } @@ -1039,12 +1033,9 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU }); }; -export const addFusionTreasures = ( - inventory: TInventoryDatabaseDocument, - itemsArray: IFusionTreasure[] | undefined -): void => { +export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsArray: IFusionTreasure[]): void => { const { FusionTreasures } = inventory; - itemsArray?.forEach(({ ItemType, ItemCount, Sockets }) => { + itemsArray.forEach(({ ItemType, ItemCount, Sockets }) => { const itemIndex = FusionTreasures.findIndex(i => i.ItemType == ItemType && (i.Sockets || 0) == (Sockets || 0)); if (itemIndex !== -1) { @@ -1055,7 +1046,7 @@ export const addFusionTreasures = ( }); }; -export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focusXpPlus: number[] | undefined): void => { +export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focusXpPlus: number[]): void => { enum FocusType { AP_UNIVERSAL, AP_ATTACK, @@ -1069,14 +1060,12 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus AP_ANY } - if (focusXpPlus) { - inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 }; - inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; - inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; - inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; - inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; - inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; - } + inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 }; + inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; + inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; + inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; + inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; + inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; }; export const updateChallengeProgress = async ( From 79147786f68068199082e3297a000e66bd5808be Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 06:08:46 -0800 Subject: [PATCH 071/776] chore: handle a FusionTreasures entry being 0 or less (#1050) Closes #1043 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1050 --- src/services/inventoryService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index d4a408ff..41d1742a 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1040,6 +1040,11 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA if (itemIndex !== -1) { FusionTreasures[itemIndex].ItemCount += ItemCount; + if (FusionTreasures[itemIndex].ItemCount == 0) { + FusionTreasures.splice(itemIndex, 1); + } else if (FusionTreasures[itemIndex].ItemCount <= 0) { + logger.warn(`account now owns a negative amount of ${ItemType}`); + } } else { FusionTreasures.push({ ItemCount, ItemType, Sockets }); } From caec5a6cbfaf3b3ba562e42d8f5c2222492c2f5a Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 06:47:34 -0800 Subject: [PATCH 072/776] feat: recipes that consume weapons (#1032) Closes #720 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1032 --- .../api/claimCompletedRecipeController.ts | 37 +++++++++--- src/controllers/api/startRecipeController.ts | 51 +++++++++++------ src/models/inventoryModels/inventoryModel.ts | 56 ++++++++++--------- src/types/inventoryTypes/inventoryTypes.ts | 18 +++--- 4 files changed, 106 insertions(+), 56 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index d8208f7f..47d606bf 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -8,6 +8,9 @@ import { IOid } from "@/src/types/commonTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; export interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -37,15 +40,35 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } if (req.query.cancel) { - const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false); - addMiscItems(inventory, recipe.ingredients); + const inventoryChanges: IInventoryChanges = { + ...updateCurrency(inventory, recipe.buildPrice * -1, false) + }; + + const nonMiscItemIngredients = new Set(); + for (const category of ["LongGuns", "Pistols", "Melee"] as const) { + if (pendingRecipe[category]) { + pendingRecipe[category].forEach(item => { + const index = inventory[category].push(item) - 1; + inventoryChanges[category] ??= []; + inventoryChanges[category].push(inventory[category][index].toJSON()); + nonMiscItemIngredients.add(item.ItemType); + + inventoryChanges.WeaponBin ??= { Slots: 0 }; + inventoryChanges.WeaponBin.Slots -= 1; + }); + } + } + const miscItemChanges: IMiscItem[] = []; + recipe.ingredients.forEach(ingredient => { + if (!nonMiscItemIngredients.has(ingredient.ItemType)) { + miscItemChanges.push(ingredient); + } + }); + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; await inventory.save(); - // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. - res.json({ - ...currencyChanges, - MiscItems: recipe.ingredients - }); + res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. } else { logger.debug("Claiming Recipe", { recipe, pendingRecipe }); diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index 642d0cfc..d2f37230 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -7,6 +7,8 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven import { unixTimesInMs } from "@/src/constants/timeConstants"; import { Types } from "mongoose"; import { ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { ExportWeapons } from "warframe-public-export-plus"; interface IStartRecipeRequest { RecipeName: string; @@ -26,23 +28,40 @@ export const startRecipeController: RequestHandler = async (req, res) => { throw new Error(`unknown recipe ${recipeName}`); } - const ingredientsInverse = recipe.ingredients.map(component => ({ - ItemType: component.ItemType, - ItemCount: component.ItemCount * -1 - })); - const inventory = await getInventory(accountId); updateCurrency(inventory, recipe.buildPrice, false); - addMiscItems(inventory, ingredientsInverse); - //buildtime is in seconds - const completionDate = new Date(Date.now() + recipe.buildTime * unixTimesInMs.second); + const pr = + inventory.PendingRecipes[ + inventory.PendingRecipes.push({ + ItemType: recipeName, + CompletionDate: new Date(Date.now() + recipe.buildTime * unixTimesInMs.second), + _id: new Types.ObjectId() + }) - 1 + ]; - inventory.PendingRecipes.push({ - ItemType: recipeName, - CompletionDate: completionDate, - _id: new Types.ObjectId() - }); + for (let i = 0; i != recipe.ingredients.length; ++i) { + if (startRecipeRequest.Ids[i]) { + const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; + if (category != "LongGuns" && category != "Pistols" && category != "Melee") { + throw new Error(`unexpected equipment ingredient type: ${category}`); + } + const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); + if (equipmentIndex == -1) { + throw new Error(`could not find equipment item to use for recipe`); + } + pr[category] ??= []; + pr[category].push(inventory[category][equipmentIndex]); + inventory[category].splice(equipmentIndex, 1); + } else { + addMiscItems(inventory, [ + { + ItemType: recipe.ingredients[i].ItemType, + ItemCount: recipe.ingredients[i].ItemCount * -1 + } + ]); + } + } if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { const spectreLoadout: ISpectreLoadout = { @@ -98,9 +117,7 @@ export const startRecipeController: RequestHandler = async (req, res) => { } } - const newInventory = await inventory.save(); + await inventory.save(); - res.json({ - RecipeId: { $oid: newInventory.PendingRecipes[newInventory.PendingRecipes.length - 1]._id.toString() } - }); + res.json({ RecipeId: toOid(pr._id) }); }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5ddfbdec..1d81bbca 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -9,8 +9,8 @@ import { ISlots, IMailboxDatabase, IDuviriInfo, - IPendingRecipe as IPendingRecipeDatabase, - IPendingRecipeResponse, + IPendingRecipeDatabase, + IPendingRecipeClient, ITypeCount, IFocusXP, IFocusUpgrade, @@ -108,29 +108,6 @@ const focusUpgradeSchema = new Schema( { _id: false } ); -const pendingRecipeSchema = new Schema( - { - ItemType: String, - CompletionDate: Date - }, - { id: false } -); - -pendingRecipeSchema.virtual("ItemId").get(function () { - return { $oid: this._id.toString() }; -}); - -pendingRecipeSchema.set("toJSON", { - virtuals: true, - transform(_document, returnedObject) { - delete returnedObject._id; - delete returnedObject.__v; - (returnedObject as IPendingRecipeResponse).CompletionDate = { - $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } - }; - } -}); - const polaritySchema = new Schema( { Slot: Number, @@ -865,6 +842,35 @@ equipmentKeys.forEach(key => { equipmentFields[key] = { type: [EquipmentSchema] }; }); +const pendingRecipeSchema = new Schema( + { + ItemType: String, + CompletionDate: Date, + LongGuns: { type: [EquipmentSchema], default: undefined }, + Pistols: { type: [EquipmentSchema], default: undefined }, + Melee: { type: [EquipmentSchema], default: undefined } + }, + { id: false } +); + +pendingRecipeSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +pendingRecipeSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject) { + delete returnedObject._id; + delete returnedObject.__v; + delete returnedObject.LongGuns; + delete returnedObject.Pistols; + delete returnedObject.Melees; + (returnedObject as IPendingRecipeClient).CompletionDate = { + $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } + }; + } +}); + const infestedFoundrySchema = new Schema( { Name: String, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index ed6da027..6fb8d2af 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -49,7 +49,7 @@ export interface IInventoryDatabase LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population Mailbox?: IMailboxDatabase; GuildId?: Types.ObjectId; - PendingRecipes: IPendingRecipe[]; + PendingRecipes: IPendingRecipeDatabase[]; QuestKeys: IQuestKeyDatabase[]; BlessingCooldown?: Date; Ships: Types.ObjectId[]; @@ -143,10 +143,6 @@ export type TSolarMapRegion = //TODO: perhaps split response and database into their own files -export interface IPendingRecipeResponse extends Omit { - CompletionDate: IMongoDate; -} - export interface IDailyAffiliations { DailyAffiliation: number; DailyAffiliationPvp: number; @@ -217,7 +213,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu XPInfo: ITypeXPItem[]; Recipes: ITypeCount[]; WeaponSkins: IWeaponSkinClient[]; - PendingRecipes: IPendingRecipeResponse[]; + PendingRecipes: IPendingRecipeClient[]; TrainingDate: IMongoDate; PlayerLevel: number; Staff?: boolean; @@ -783,12 +779,20 @@ export interface IPendingCouponClient { Discount: number; } -export interface IPendingRecipe { +export interface IPendingRecipeDatabase { ItemType: string; CompletionDate: Date; ItemId: IOid; TargetItemId?: string; // likely related to liches TargetFingerprint?: string; // likely related to liches + LongGuns?: IEquipmentDatabase[]; + Pistols?: IEquipmentDatabase[]; + Melee?: IEquipmentDatabase[]; +} + +export interface IPendingRecipeClient + extends Omit { + CompletionDate: IMongoDate; } export interface IPendingTrade { From 4205364bd8b39c0c68f53591fa3071698850fb53 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 12:35:14 -0800 Subject: [PATCH 073/776] feat: noDojoResearchCosts & noDojoResearchTime (#1053) Closes #1052 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1053 Co-authored-by: Sainan Co-committed-by: Sainan --- config.json.example | 2 ++ src/controllers/api/guildTechController.ts | 36 ++++++++++++++-------- src/services/configService.ts | 2 ++ static/webui/index.html | 8 +++++ static/webui/translations/en.js | 2 ++ static/webui/translations/ru.js | 2 ++ 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/config.json.example b/config.json.example index 1bd3b1c1..04add3df 100644 --- a/config.json.example +++ b/config.json.example @@ -31,5 +31,7 @@ "unlockExilusEverywhere": true, "unlockArcanesEverywhere": true, "noDailyStandingLimits": true, + "noDojoResearchCosts": true, + "noDojoResearchTime": true, "spoofMasteryRank": -1 } diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index b9d3b75d..129131d6 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,10 +1,12 @@ import { RequestHandler } from "express"; import { getGuildForRequestEx } from "@/src/services/guildService"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { config } from "@/src/services/configService"; +import { ITechProjectDatabase } from "@/src/types/guildTypes"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -20,15 +22,21 @@ export const guildTechController: RequestHandler = async (req, res) => { const recipe = ExportDojoRecipes.research[data.RecipeType!]; guild.TechProjects ??= []; if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { - guild.TechProjects.push({ - ItemType: data.RecipeType!, - ReqCredits: scaleRequiredCount(recipe.price), - ReqItems: recipe.ingredients.map(x => ({ - ItemType: x.ItemType, - ItemCount: scaleRequiredCount(x.ItemCount) - })), - State: 0 - }); + const techProject = + guild.TechProjects[ + guild.TechProjects.push({ + ItemType: data.RecipeType!, + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price), + ReqItems: recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount) + })), + State: 0 + }) - 1 + ]; + if (config.noDojoResearchCosts) { + processFundedProject(techProject, recipe); + } } await guild.save(); res.end(); @@ -59,9 +67,8 @@ export const guildTechController: RequestHandler = async (req, res) => { if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { // This research is now fully funded. - techProject.State = 1; const recipe = ExportDojoRecipes.research[data.RecipeType!]; - techProject.CompletionDate = new Date(new Date().getTime() + recipe.time * 1000); + processFundedProject(techProject, recipe); } await guild.save(); @@ -98,6 +105,11 @@ export const guildTechController: RequestHandler = async (req, res) => { } }; +const processFundedProject = (techProject: ITechProjectDatabase, recipe: IDojoResearch): void => { + techProject.State = 1; + techProject.CompletionDate = new Date(new Date().getTime() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); +}; + type TGuildTechRequest = { Action: string; } & Partial & diff --git a/src/services/configService.ts b/src/services/configService.ts index e1b9c984..466b062c 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -57,6 +57,8 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noDojoResearchCosts?: boolean; + noDojoResearchTime?: boolean; spoofMasteryRank?: number; } diff --git a/static/webui/index.html b/static/webui/index.html index 0d4b3e95..57e35b47 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -517,6 +517,14 @@
+
+ + +
+
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index f69a8fd3..dd8f5d0b 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -111,6 +111,8 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_noDojoResearchCosts: `No Dojo Research Costs`, + cheats_noDojoResearchTime: `No Dojo Research Time`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, cheats_saveSettings: `Save Settings`, cheats_account: `Account`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index d2cc9a02..a8a2e3ac 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -112,6 +112,8 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, + cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, cheats_saveSettings: `Сохранить настройки`, cheats_account: `Аккаунт`, From 8c662fa1ec082586b23870969ebb6ceb05c478a8 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 12:36:01 -0800 Subject: [PATCH 074/776] feat: derelict vault rewards (#1049) Closes #1045 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1049 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index d52b6b7f..c1803a64 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -7,7 +7,7 @@ import { } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { IRngResult, getRandomReward } from "@/src/services/rngService"; +import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addChallenges, @@ -307,6 +307,13 @@ export const addMissionRewards = async ( } } + if (rewardInfo.useVaultManifest) { + MissionRewards.push({ + StoreItem: getRandomElement(corruptedMods), + ItemCount: 1 + }); + } + for (const reward of MissionRewards) { const inventoryChange = await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount); //TODO: combineInventoryChanges improve type safety, merging 2 of the same item? @@ -468,3 +475,30 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] { } return drops; } + +const corruptedMods = [ + "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedHeavyDamageChargeSpeedMod", // Corrupt Charge + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritDamagePistol", // Hollow Point + "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedDamageSpeedMod", // Spoiled Strike + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedDamageRecoilPistol", // Magnum Force + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedMaxClipReloadSpeedPistol", // Tainted Clip + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedCritRateFireRateRifle", // Critical Delay + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedDamageRecoilRifle", // Heavy Caliber + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedMaxClipReloadSpeedRifle", // Tainted Mag + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedRecoilFireRateRifle", // Vile Precision + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedDurationRangeWarframe", // Narrow Minded + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedEfficiencyDurationWarframe", // Fleeting Expertise + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerEfficiencyWarframe", // Blind Rage + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedRangePowerWarframe", // Overextended + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedAccuracyFireRateShotgun", // Tainted Shell + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedDamageAccuracyShotgun", // Vicious Spread + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedMaxClipReloadSpeedShotgun", // Burdened Magazine + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedFireRateDamagePistol", // Anemic Agility + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedFireRateDamageRifle", // Vile Acceleration + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedFireRateDamageShotgun", // Frail Momentum + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedCritChanceFireRateShotgun", // Critical Deceleration + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritChanceFireRatePistol", // Creeping Bullseye + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerStrengthPowerDurationWarframe", // Transient Fortitude + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedReloadSpeedMaxClipRifle", // Depleted Reload + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/FixedShieldAndShieldGatingDuration" // Catalyzing Shields +]; From c971a484ef9e034b299bf71f1c578fb63b6822ff Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 12:36:49 -0800 Subject: [PATCH 075/776] feat: updateAlignment (#1039) Closes #1038 may need some more information about how this endpoint works. had to make a few assumptions. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1039 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/updateAlignmentController.ts | 25 +++++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 15 ++++++++--- src/routes/api.ts | 2 ++ src/types/inventoryTypes/inventoryTypes.ts | 4 +-- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/controllers/api/updateAlignmentController.ts diff --git a/src/controllers/api/updateAlignmentController.ts b/src/controllers/api/updateAlignmentController.ts new file mode 100644 index 00000000..2942ad6f --- /dev/null +++ b/src/controllers/api/updateAlignmentController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IAlignment } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const updateAlignmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const body = getJSONfromString(String(req.body)); + inventory.Alignment = { + Alignment: body.Alignment, + Wisdom: body.Wisdom + }; + await inventory.save(); + res.json(inventory.Alignment); +}; + +interface IUpdateAlignmentRequest { + Wisdom: number; + Alignment: number; + PreviousAlignment: IAlignment; + AlignmentAction: string; // e.g. "/Lotus/Language/Game/MawCinematicDualChoice" + KeyChainName: string; +} diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 1d81bbca..8d3f871b 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -70,7 +70,8 @@ import { IPendingCouponClient, ILibraryDailyTaskInfo, IDroneDatabase, - IDroneClient + IDroneClient, + IAlignment } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -970,6 +971,14 @@ const libraryDailyTaskInfoSchema = new Schema( { _id: false } ); +const alignmentSchema = new Schema( + { + Alignment: Number, + Wisdom: Number + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1171,8 +1180,8 @@ const inventorySchema = new Schema( //https://warframe.fandom.com/wiki/Alignment //like "Alignment": { "Wisdom": 9, "Alignment": 1 }, - Alignment: Schema.Types.Mixed, - AlignmentReplay: Schema.Types.Mixed, + Alignment: alignmentSchema, + AlignmentReplay: alignmentSchema, //https://warframe.fandom.com/wiki/Sortie CompletedSorties: [String], diff --git a/src/routes/api.ts b/src/routes/api.ts index 335c59df..0bc16ccf 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -87,6 +87,7 @@ import { syndicateStandingBonusController } from "@/src/controllers/api/syndicat import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController"; import { trainingResultController } from "@/src/controllers/api/trainingResultController"; import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController"; +import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController"; import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController"; import { updateQuestController } from "@/src/controllers/api/updateQuestController"; import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController"; @@ -188,6 +189,7 @@ apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); apiRouter.post("/tauntHistory.php", tauntHistoryController); apiRouter.post("/trainingResult.php", trainingResultController); apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController); +apiRouter.post("/updateAlignment.php", updateAlignmentController); apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController); apiRouter.post("/updateNodeIntros.php", genericUpdateController); apiRouter.post("/updateQuest.php", updateQuestController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 6fb8d2af..3472f692 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -253,7 +253,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CompletedSyndicates: string[]; FocusXP?: IFocusXP; Wishlist: string[]; - Alignment: IAlignment; + Alignment?: IAlignment; CompletedSorties: string[]; LastSortieReward: ILastSortieReward[]; Drones: IDroneClient[]; @@ -267,7 +267,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu HasContributedToDojo?: boolean; HWIDProtectEnabled?: boolean; KubrowPetPrints: IKubrowPetPrint[]; - AlignmentReplay: IAlignment; + AlignmentReplay?: IAlignment; PersonalGoalProgress: IPersonalGoalProgress[]; ThemeStyle: string; ThemeBackground: string; From 32cc8dc61b619eacdd1ec58e0c6c04b2473930c2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 12:39:51 -0800 Subject: [PATCH 076/776] fix: preinstall potatoes on non-crafted equipment (#1037) Fixes #1028 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1037 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 75 ++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 41d1742a..4bdd2296 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -36,7 +36,12 @@ import { } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { getExalted, getKeyChainItems } from "@/src/services/itemDataService"; -import { IEquipmentClient, IEquipmentDatabase, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; +import { + EquipmentFeatures, + IEquipmentClient, + IEquipmentDatabase, + IItemConfig +} from "../types/inventoryTypes/commonInventoryTypes"; import { ExportArcanes, ExportBundles, @@ -341,7 +346,14 @@ export const addItem = async ( if (typeName in ExportWeapons) { const weapon = ExportWeapons[typeName]; if (weapon.totalDamage != 0) { - const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName); + const inventoryChanges = addEquipment( + inventory, + weapon.productCategory, + typeName, + [], + {}, + premiumPurchase ? { Features: EquipmentFeatures.DOUBLE_CAPACITY } : {} + ); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { combineInventoryChanges(inventoryChanges, (await addItem(inventory, item, 1)).InventoryChanges); @@ -421,7 +433,12 @@ export const addItem = async ( default: { return { InventoryChanges: { - ...addPowerSuit(inventory, typeName), + ...addPowerSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) } }; @@ -429,7 +446,12 @@ export const addItem = async ( case "Archwing": { return { InventoryChanges: { - ...addSpaceSuit(inventory, typeName), + ...addSpaceSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), ...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase) } }; @@ -437,7 +459,12 @@ export const addItem = async ( case "EntratiMech": { return { InventoryChanges: { - ...addMechSuit(inventory, typeName), + ...addMechSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) } }; @@ -471,7 +498,12 @@ export const addItem = async ( case "Sentinels": { return { InventoryChanges: { - ...addSentinel(inventory, typeName), + ...addSentinel( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), ...occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase) } }; @@ -572,7 +604,8 @@ export const applyDefaultUpgrades = ( export const addSentinel = ( inventory: TInventoryDatabaseDocument, sentinelName: string, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined ): IInventoryChanges => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportSentinels[sentinelName]?.defaultWeapon) { @@ -582,7 +615,8 @@ export const addSentinel = ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades); - const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0 }) - 1; + const sentinelIndex = + inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features }) - 1; inventoryChanges.Sentinels ??= []; inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON()); @@ -602,7 +636,8 @@ export const addSentinelWeapon = ( export const addPowerSuit = ( inventory: TInventoryDatabaseDocument, powersuitName: string, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined ): IInventoryChanges => { const specialItems = getExalted(powersuitName); if (specialItems) { @@ -610,7 +645,8 @@ export const addPowerSuit = ( addSpecialItem(inventory, specialItem, inventoryChanges); } } - const suitIndex = inventory.Suits.push({ ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; + const suitIndex = + inventory.Suits.push({ ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0, Features: features }) - 1; inventoryChanges.Suits ??= []; inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON()); return inventoryChanges; @@ -619,7 +655,8 @@ export const addPowerSuit = ( export const addMechSuit = ( inventory: TInventoryDatabaseDocument, mechsuitName: string, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined ): IInventoryChanges => { const specialItems = getExalted(mechsuitName); if (specialItems) { @@ -627,7 +664,9 @@ export const addMechSuit = ( addSpecialItem(inventory, specialItem, inventoryChanges); } } - const suitIndex = inventory.MechSuits.push({ ItemType: mechsuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; + const suitIndex = + inventory.MechSuits.push({ ItemType: mechsuitName, Configs: [], UpgradeVer: 101, XP: 0, Features: features }) - + 1; inventoryChanges.MechSuits ??= []; inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); return inventoryChanges; @@ -656,9 +695,17 @@ export const addSpecialItem = ( export const addSpaceSuit = ( inventory: TInventoryDatabaseDocument, spacesuitName: string, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined ): IInventoryChanges => { - const suitIndex = inventory.SpaceSuits.push({ ItemType: spacesuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; + const suitIndex = + inventory.SpaceSuits.push({ + ItemType: spacesuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features + }) - 1; inventoryChanges.SpaceSuits ??= []; inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON()); return inventoryChanges; From da2b50d537d7093df2bb3b19d4d4a48d910dd248 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 18:09:37 -0800 Subject: [PATCH 077/776] feat: collectible series (#1025) Closes #712 a bit unsure about the inbox messages, but otherwise it should be working Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1025 --- .../api/startCollectibleEntryController.ts | 27 ++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 26 +++++++++++++-- src/routes/api.ts | 2 ++ src/services/missionInventoryUpdateService.ts | 32 +++++++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 4 +-- src/types/requestTypes.ts | 4 ++- .../kuriaMessages/fiftyPercent.json | 7 ++++ .../kuriaMessages/oneHundredPercent.json | 8 +++++ .../kuriaMessages/seventyFivePercent.json | 7 ++++ 9 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/controllers/api/startCollectibleEntryController.ts create mode 100644 static/fixed_responses/kuriaMessages/fiftyPercent.json create mode 100644 static/fixed_responses/kuriaMessages/oneHundredPercent.json create mode 100644 static/fixed_responses/kuriaMessages/seventyFivePercent.json diff --git a/src/controllers/api/startCollectibleEntryController.ts b/src/controllers/api/startCollectibleEntryController.ts new file mode 100644 index 00000000..ffc440c1 --- /dev/null +++ b/src/controllers/api/startCollectibleEntryController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IIncentiveState } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const startCollectibleEntryController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const request = getJSONfromString(String(req.body)); + inventory.CollectibleSeries ??= []; + inventory.CollectibleSeries.push({ + CollectibleType: request.target, + Count: 0, + Tracking: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ReqScans: request.reqScans, + IncentiveStates: request.other + }); + await inventory.save(); + res.status(200).end(); +}; + +interface IStartCollectibleEntryRequest { + target: string; + reqScans: number; + other: IIncentiveState[]; +} diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 8d3f871b..31ada0e8 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -71,7 +71,9 @@ import { ILibraryDailyTaskInfo, IDroneDatabase, IDroneClient, - IAlignment + IAlignment, + ICollectibleEntry, + IIncentiveState } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -943,6 +945,26 @@ const calenderProgressSchema = new Schema( { _id: false } ); +const incentiveStateSchema = new Schema( + { + threshold: Number, + complete: Boolean, + sent: Boolean + }, + { _id: false } +); + +const collectibleEntrySchema = new Schema( + { + CollectibleType: String, + Count: Number, + Tracking: String, + ReqScans: Number, + IncentiveStates: [incentiveStateSchema] + }, + { _id: false } +); + const pendingCouponSchema = new Schema( { Expiry: { type: Date, default: new Date(0) }, @@ -1286,7 +1308,7 @@ const inventorySchema = new Schema( RecentVendorPurchases: [Schema.Types.Mixed], Robotics: [Schema.Types.Mixed], UsedDailyDeals: [Schema.Types.Mixed], - CollectibleSeries: [Schema.Types.Mixed], + CollectibleSeries: { type: [collectibleEntrySchema], default: undefined }, HasResetAccount: { type: Boolean, default: false }, //Discount Coupon diff --git a/src/routes/api.ts b/src/routes/api.ts index 0bc16ccf..dafa2a36 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -76,6 +76,7 @@ import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShip import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController"; import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController"; import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController"; +import { startCollectibleEntryController } from "@/src/controllers/api/startCollectibleEntryController"; import { startDojoRecipeController } from "@/src/controllers/api/startDojoRecipeController"; import { startLibraryDailyTaskController } from "@/src/controllers/api/startLibraryDailyTaskController"; import { startLibraryPersonalTargetController } from "@/src/controllers/api/startLibraryPersonalTargetController"; @@ -181,6 +182,7 @@ apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController); apiRouter.post("/shipDecorations.php", shipDecorationsController); +apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController); apiRouter.post("/startDojoRecipe.php", startDojoRecipeController); apiRouter.post("/startRecipe.php", startRecipeController); apiRouter.post("/stepSequencers.php", stepSequencersController); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c1803a64..64814e4b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -34,6 +34,10 @@ import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryType import { handleStoreItemAcquisition } from "./purchaseService"; import { IMissionReward } from "../types/missionTypes"; import { crackRelic } from "@/src/helpers/relicHelper"; +import { createMessage } from "./inboxService"; +import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json"; +import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; +import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -201,6 +205,34 @@ export const addMissionInventoryUpdates = ( } }); break; + case "CollectibleScans": + value.forEach(scan => { + const entry = inventory.CollectibleSeries?.find(x => x.CollectibleType == scan.CollectibleType); + if (entry) { + entry.Count = scan.Count; + entry.Tracking = scan.Tracking; + if (entry.CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne") { + const progress = entry.Count / entry.ReqScans; + entry.IncentiveStates.forEach(gate => { + gate.complete = progress >= gate.threshold; + if (gate.complete && !gate.sent) { + gate.sent = true; + if (gate.threshold == 0.5) { + void createMessage(inventory.accountOwnerId.toString(), [kuriaMessage50]); + } else { + void createMessage(inventory.accountOwnerId.toString(), [kuriaMessage75]); + } + } + }); + if (progress >= 1.0) { + void createMessage(inventory.accountOwnerId.toString(), [kuriaMessage100]); + } + } + } else { + logger.warn(`${scan.CollectibleType} was not found in inventory, ignoring scans`); + } + }); + break; case "Upgrades": value.forEach(clientUpgrade => { const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 3472f692..e044a1b7 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -315,7 +315,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu UsedDailyDeals: any[]; LibraryPersonalTarget: string; LibraryPersonalProgress: ILibraryPersonalProgress[]; - CollectibleSeries: ICollectibleSery[]; + CollectibleSeries?: ICollectibleEntry[]; LibraryAvailableDailyTaskInfo?: ILibraryDailyTaskInfo; LibraryActiveDailyTaskInfo?: ILibraryDailyTaskInfo; HasResetAccount: boolean; @@ -364,7 +364,7 @@ export interface IChallengeProgress { Completed?: string[]; } -export interface ICollectibleSery { +export interface ICollectibleEntry { CollectibleType: string; Count: number; Tracking: string; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 9ebf3adb..2584bd77 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -15,7 +15,8 @@ import { IPlayerSkills, IQuestKeyDatabase, ILoreFragmentScan, - IUpgradeClient + IUpgradeClient, + ICollectibleEntry } from "./inventoryTypes/inventoryTypes"; export interface IThemeUpdateRequest { @@ -99,6 +100,7 @@ export type IMissionInventoryUpdateRequest = { CodexScanCount: number; Standing: number; }[]; + CollectibleScans?: ICollectibleEntry[]; Upgrades?: IUpgradeClient[]; // riven challenge progress } & { [K in TEquipmentKey]?: IEquipmentClient[]; diff --git a/static/fixed_responses/kuriaMessages/fiftyPercent.json b/static/fixed_responses/kuriaMessages/fiftyPercent.json new file mode 100644 index 00000000..8466a215 --- /dev/null +++ b/static/fixed_responses/kuriaMessages/fiftyPercent.json @@ -0,0 +1,7 @@ +{ + "sub": "/Lotus/Language/Oddities/SeriesOne50PercentInboxMessageSubject", + "sndr": "/Lotus/Language/Menu/ScribeName", + "msg": "/Lotus/Language/Oddities/SeriesOne50PercentInboxMessage", + "icon": "/Lotus/Interface/Icons/Syndicates/FactionOddityGold.png", + "att": ["/Lotus/Upgrades/Skins/Clan/OrokittyBadgeItem"] +} diff --git a/static/fixed_responses/kuriaMessages/oneHundredPercent.json b/static/fixed_responses/kuriaMessages/oneHundredPercent.json new file mode 100644 index 00000000..3e73e97a --- /dev/null +++ b/static/fixed_responses/kuriaMessages/oneHundredPercent.json @@ -0,0 +1,8 @@ +{ + "sub": "/Lotus/Language/Oddities/SeriesOneRewardSubject", + "sndr": "/Lotus/Language/Menu/ScribeName", + "msg": "/Lotus/Language/Oddities/SeriesOneRewardInboxMessage", + "icon": "/Lotus/Interface/Icons/Syndicates/FactionOddityGold.png", + "att": ["/Lotus/Types/Items/ShipDecos/OrokinFelisBobbleHead"], + "highPriority": true +} diff --git a/static/fixed_responses/kuriaMessages/seventyFivePercent.json b/static/fixed_responses/kuriaMessages/seventyFivePercent.json new file mode 100644 index 00000000..fe496790 --- /dev/null +++ b/static/fixed_responses/kuriaMessages/seventyFivePercent.json @@ -0,0 +1,7 @@ +{ + "sub": "/Lotus/Language/Oddities/SeriesOne75PercentInboxMessageSubject", + "sndr": "/Lotus/Language/Menu/ScribeName", + "msg": "/Lotus/Language/Oddities/SeriesOne75PercentInboxMessage", + "icon": "/Lotus/Interface/Icons/Syndicates/FactionOddityGold.png", + "att": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageOroKitty"] +} From bbc40d55341aafc8cc0f434ec827a739e6d01d16 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 28 Feb 2025 18:18:33 -0800 Subject: [PATCH 078/776] feat: updateSongChallenge (#1024) Closes #707 untested but should be correct based on all the information I could find Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1024 --- .../api/updateSongChallengeController.ts | 50 +++++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 15 +++++- src/routes/api.ts | 2 + src/types/inventoryTypes/inventoryTypes.ts | 6 +++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/updateSongChallengeController.ts diff --git a/src/controllers/api/updateSongChallengeController.ts b/src/controllers/api/updateSongChallengeController.ts new file mode 100644 index 00000000..e0a10fc8 --- /dev/null +++ b/src/controllers/api/updateSongChallengeController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +export const updateSongChallengeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const request = getJSONfromString(String(req.body)); + inventory.SongChallenges ??= []; + let songChallenge = inventory.SongChallenges.find(x => x.Song == request.Song); + if (!songChallenge) { + songChallenge = + inventory.SongChallenges[inventory.SongChallenges.push({ Song: request.Song, Difficulties: [] }) - 1]; + } + songChallenge.Difficulties.push(request.Difficulty); + + const response: IUpdateSongChallengeResponse = { + Song: request.Song, + Difficulty: request.Difficulty + }; + + // Handle all songs being completed on all difficulties + if (inventory.SongChallenges.length == 12 && !inventory.SongChallenges.find(x => x.Difficulties.length != 2)) { + response.Reward = "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropShawzinDuviri"; + const shipDecorationChanges = [ + { ItemType: "/Lotus/Types/Items/ShipDecos/LisetPropShawzinDuviri", ItemCount: 1 } + ]; + response.InventoryChanges = { + ShipDecorations: shipDecorationChanges + }; + addShipDecorations(inventory, shipDecorationChanges); + } + + await inventory.save(); + res.json(response); +}; + +interface IUpdateSongChallengeRequest { + Song: string; + Difficulty: number; +} + +interface IUpdateSongChallengeResponse { + Song: string; + Difficulty: number; + Reward?: string; + InventoryChanges?: IInventoryChanges; +} diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 31ada0e8..7c7645a5 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -73,7 +73,8 @@ import { IDroneClient, IAlignment, ICollectibleEntry, - IIncentiveState + IIncentiveState, + ISongChallenge } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -965,6 +966,14 @@ const collectibleEntrySchema = new Schema( { _id: false } ); +const songChallengeSchema = new Schema( + { + Song: String, + Difficulties: [Number] + }, + { _id: false } +); + const pendingCouponSchema = new Schema( { Expiry: { type: Date, default: new Date(0) }, @@ -1323,7 +1332,9 @@ const inventorySchema = new Schema( EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, DialogueHistory: dialogueHistorySchema, - CalendarProgress: calenderProgressSchema + CalendarProgress: calenderProgressSchema, + + SongChallenges: { type: [songChallengeSchema], default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); diff --git a/src/routes/api.ts b/src/routes/api.ts index dafa2a36..af8de31a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -92,6 +92,7 @@ import { updateAlignmentController } from "@/src/controllers/api/updateAlignment import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController"; import { updateQuestController } from "@/src/controllers/api/updateQuestController"; import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController"; +import { updateSongChallengeController } from "@/src/controllers/api/updateSongChallengeController"; import { updateThemeController } from "@/src/controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; @@ -196,6 +197,7 @@ apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController apiRouter.post("/updateNodeIntros.php", genericUpdateController); apiRouter.post("/updateQuest.php", updateQuestController); apiRouter.post("/updateSession.php", updateSessionPostController); +apiRouter.post("/updateSongChallenge.php", updateSongChallengeController); apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/upgrades.php", upgradesController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e044a1b7..d81491c1 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -325,6 +325,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EndlessXP?: IEndlessXpProgress[]; DialogueHistory?: IDialogueHistoryClient; CalendarProgress: ICalendarProgress; + SongChallenges?: ISongChallenge[]; } export interface IAffiliation { @@ -1079,3 +1080,8 @@ export interface ICalendarProgress { YearProgress: { Upgrades: unknown[] }; SeasonProgress: ISeasonProgress; } + +export interface ISongChallenge { + Song: string; + Difficulties: number[]; +} From 70cd088ffaff854863c1c5e36b3ece82e00674e5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 2 Mar 2025 12:04:22 +0100 Subject: [PATCH 079/776] chore: exclude markdown files from prettier --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index ab38eac9..59d6af97 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ static/webui/libs/ *.html +*.md From 915820905927f3f5a3bb92dfc037e23435cb57d2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 2 Mar 2025 04:18:59 -0800 Subject: [PATCH 080/776] feat: handle acquisition of EmailItems (#1064) Fixes #1063 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1064 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 11 ++++++++++- src/services/itemDataService.ts | 19 +++++++++++++++++-- src/services/questService.ts | 15 +-------------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 4bdd2296..398cf10c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -35,7 +35,7 @@ import { IUpdateChallengeProgressRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { getExalted, getKeyChainItems } from "@/src/services/itemDataService"; +import { convertInboxMessage, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; import { EquipmentFeatures, IEquipmentClient, @@ -47,6 +47,7 @@ import { ExportBundles, ExportCustoms, ExportDrones, + ExportEmailItems, ExportEnemies, ExportFlavour, ExportFusionBundles, @@ -71,6 +72,7 @@ import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { getRandomElement, getRandomInt } from "./rngService"; +import { createMessage } from "./inboxService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -425,6 +427,13 @@ export const addItem = async ( InventoryChanges: inventoryChanges }; } + if (typeName in ExportEmailItems) { + const emailItem = ExportEmailItems[typeName]; + await createMessage(inventory.accountOwnerId.toString(), [convertInboxMessage(emailItem.message)]); + return { + InventoryChanges: {} + }; + } // Path-based duck typing switch (typeName.substr(1).split("/")[1]) { diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index f24f5032..8fd3969d 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -36,6 +36,7 @@ import { TReward } from "warframe-public-export-plus"; import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; +import { IMessage } from "../models/inboxModel"; export type WeaponTypeInternal = | "LongGuns" @@ -207,7 +208,7 @@ export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefi return items; }; -export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IInboxMessage => { +export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IMessage => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[KeyChain]?.chainStages; if (!chainStages) { @@ -227,5 +228,19 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist` ); } - return chainStageMessage; + return convertInboxMessage(chainStageMessage); +}; + +export const convertInboxMessage = (message: IInboxMessage): IMessage => { + return { + sndr: message.sender, + msg: message.body, + sub: message.title, + att: message.attachments.length > 0 ? message.attachments : undefined, + countedAtt: message.countedAttachments.length > 0 ? message.countedAttachments : undefined, + icon: message.icon ?? "", + transmission: message.transmission ?? "", + highPriority: message.highPriority ?? false, + r: false + } satisfies IMessage; }; diff --git a/src/services/questService.ts b/src/services/questService.ts index d0bdd9db..84d42f27 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -1,6 +1,5 @@ import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { isEmptyObject } from "@/src/helpers/general"; -import { IMessage } from "@/src/models/inboxModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; import { addItem, addKeyChainItems } from "@/src/services/inventoryService"; @@ -198,19 +197,7 @@ export const giveKeyChainMessage = async ( ): Promise => { const keyChainMessage = getKeyChainMessage(keyChainInfo); - const message = { - sndr: keyChainMessage.sender, - msg: keyChainMessage.body, - sub: keyChainMessage.title, - att: keyChainMessage.attachments.length > 0 ? keyChainMessage.attachments : undefined, - countedAtt: keyChainMessage.countedAttachments.length > 0 ? keyChainMessage.countedAttachments : undefined, - icon: keyChainMessage.icon ?? "", - transmission: keyChainMessage.transmission ?? "", - highPriority: keyChainMessage.highPriority ?? false, - r: false - } satisfies IMessage; - - await createMessage(accountId, [message]); + await createMessage(accountId, [keyChainMessage]); updateQuestStage(inventory, keyChainInfo, { m: true }); }; From 8a6f36a9b0bd91a524bada4fe81dc47393608103 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 2 Mar 2025 04:21:59 -0800 Subject: [PATCH 081/776] feat(webui): add relics via "add items" (#1066) Closes #1062 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1066 Co-authored-by: Sainan Co-committed-by: Sainan --- .../custom/getItemListsController.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index ce80d041..6534bf92 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -6,12 +6,14 @@ import { ExportGear, ExportMisc, ExportRecipes, + ExportRelics, ExportResources, ExportSentinels, ExportSyndicates, ExportUpgrades, ExportWarframes, - ExportWeapons + ExportWeapons, + TRelicQuality } from "warframe-public-export-plus"; import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json"; @@ -23,6 +25,13 @@ interface ListedItem { badReason?: "starter" | "frivolous" | "notraw"; } +const relicQualitySuffixes: Record = { + VPQ_BRONZE: "", + VPQ_SILVER: " [Flawless]", + VPQ_GOLD: " [Radiant]", + VPQ_PLATINUM: " [Exceptional]" +}; + const getItemListsController: RequestHandler = (req, response) => { const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const res: Record = {}; @@ -108,9 +117,22 @@ const getItemListsController: RequestHandler = (req, response) => { name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); } } + if (uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/") { + res.miscitems.push({ + uniqueName: item.productCategory + ":" + uniqueName, + name: name + }); + } + } + for (const [uniqueName, item] of Object.entries(ExportRelics)) { res.miscitems.push({ - uniqueName: item.productCategory + ":" + uniqueName, - name: name + uniqueName: "MiscItems:" + uniqueName, + name: + getString("/Lotus/Language/Relics/VoidProjectionName", lang) + .split("|ERA|") + .join(item.era) + .split("|CATEGORY|") + .join(item.category) + relicQualitySuffixes[item.quality] }); } for (const [uniqueName, item] of Object.entries(ExportGear)) { From 0798d8c6b43125de52ef9a4d99acd32b6c676afc Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 2 Mar 2025 04:22:20 -0800 Subject: [PATCH 082/776] chore: update cert (#1056) The *.p2ptls.com cert expires on April 16, so I'm changing it for *.viatls.com which expires on August 3. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1056 Co-authored-by: Sainan Co-committed-by: Sainan --- static/certs/cert.pem | 86 ++++++++++++++----------------------------- static/certs/key.pem | 52 +++++++++++++------------- 2 files changed, 53 insertions(+), 85 deletions(-) diff --git a/static/certs/cert.pem b/static/certs/cert.pem index dce14b62..4f043e8a 100644 --- a/static/certs/cert.pem +++ b/static/certs/cert.pem @@ -1,38 +1,38 @@ -----BEGIN CERTIFICATE----- -MIIGLjCCBRagAwIBAgIRAPeLmReXnv+ALT/3Tm2Vts4wDQYJKoZIhvcNAQELBQAw +MIIGLzCCBRegAwIBAgIRAILIyLcitteoEGcJt1QBXvcwDQYJKoZIhvcNAQELBQAw gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD -QTAeFw0yNDA0MTUwMDAwMDBaFw0yNTA0MTUyMzU5NTlaMBcxFTATBgNVBAMMDCou -cDJwdGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKoxU6lW -K5iAXZfLrKOY5lcy7z+mML2cYZkW0XXJeC6jYDyYSGAPJogeIgd3JsJWjZvHxnj7 -8KJGjO5j8B8kz4CVcV6aEx4ExJvtFUSzkgXHhlvSo2p0TTtWxC+ib3vWv+5kBSzb -4mdKKHiaz9shcLNKB77305xSBnKjAPGElgaZRwjwMqUSbPyjx4KrehyPQZDOU0aR -TKUbQNDbKYbeEmmUku0FTpao35GNsJrwzKKFIgzWAGKY+QiywIMeOGf0dTqX60GQ -MeXkKbueibuFKA12foV8RGojdT+bPIdRQyyEyntUkbu+UMknJ9bsPbKTEyQgv5nY -62O+A2lYG89Ub7MCAwEAAaOCAvowggL2MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb -+ZsF4bgBjWHhMB0GA1UdDgQWBBQgFEQlEKO9vXkpBU7pQjbMU8MZvTAOBgNVHQ8B +QTAeFw0yNDA4MDIwMDAwMDBaFw0yNTA4MDIyMzU5NTlaMBcxFTATBgNVBAMMDCou +dmlhdGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMTToSjY +3aUIxjghIkikJfFExVwSEzIM2XaQNJE+SxQ6Cc+xUR5QJrMJnM/39sH5c5imMEUo +2OnstCIaVMPx5ZPN+HXLsvmoVAe2/xYe7emnZ5ZFTUXPyqkzDRg0hkMJiWWo/Nmf +ypZfUJoz6hVkXwsgNFPTVuo7aECQFlZslh2HQVDOfBaNBxQBaOJ5vf6nllf/aLyB +tZ74nlLynVYV9kYzISP4dUcxQ+D4HZgIxyOQfcN3EHUS1ZVaIp8hupOygF8zGQyJ +uzFozzg5I59U+hT1yQG3FlwTBnP+sA0+hW0LBTbWSISm0If1SgHlUEqxLlosjuTG +BG45h9o2bAz9po0CAwEAAaOCAvswggL3MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb ++ZsF4bgBjWHhMB0GA1UdDgQWBBQ/OeA2gLbVIKIuIitYRqSRUWMP3TAOBgNVHQ8B Af8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUFBwIBFhdo dHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUFBwEBBHgw djBPBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNB RG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYX -aHR0cDovL29jc3Auc2VjdGlnby5jb20wIwYDVR0RBBwwGoIMKi5wMnB0bHMuY29t -ggpwMnB0bHMuY29tMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgDPEVbu1S58 -r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAY7jjWjnAAAEAwBHMEUCIQD/BajQ -AYjbiSmZZaTZ1j2miDHS4onTeIwMA5/jeAYzLgIgTAoSaQnX6Niyld5gmysgfkRC -zkiI/WwEJUxmI+R3Ll4AdwCi4wrkRe+9rZt+OO1HZ3dT14JbhJTXK14bLMS5UKRH -5wAAAY7jjWiVAAAEAwBIMEYCIQC1tH+VO0bRco4oSYvfsPaJDbLoJ2vfqSrCjtqu -nLavHwIhANuDbW4fRFA/myvN7mrLm3VLHI63RTl/gnzNqxodfB5oAHUATnWjJ1ya -EMM4W2zU3z9S6x3w4I4bjWnAsfpksWKaOd8AAAGO441ojgAABAMARjBEAiAzv6zf -dPxtnecz30Rb63+UiyvT2SdmdTTP+ap3r1rpCgIgX5z8mLnJJ3WL0LIB5NRC9qPn -/t324TkyWDHKgMPom2gwDQYJKoZIhvcNAQELBQADggEBAH7mgrQLmTkMs6/F/RoE -nsHQ9ddsDAA+Fs04alH8D8kuuXSsUWhaf0OYfBHLtOZ238qfigLxXZ6oGj9qNQ0I -hMP56sjEqd2IF2Vfi/qV3igLuJcICWnqqKIegCcS4fmy90NwYVtp2Z/7ovUa8aY/ -yKGoXTfmDQwuyaH88j14Ft95lmvOJ4VPheGmSotZOaIkp1os/wPIoQAmWoecj173 -jnLQ6O5/IZC4s/xKLKVt+vW+nmyR5U7VjUqAFN8eBHgdGWRcAiEaTRLBZMwWYP2D -XPFWmwT8vkvvK0WagFYOoITH9Zu13dHHzReIEyBhCDXWYyfib8i3K+acXidmi7Lu -fAw= +aHR0cDovL29jc3Auc2VjdGlnby5jb20wIwYDVR0RBBwwGoIMKi52aWF0bHMuY29t +ggp2aWF0bHMuY29tMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdQDd3Mo0ldfh +FgXnlTL6x5/4PRxQ39sAOhQSdgosrLvIKgAAAZEVLi9VAAAEAwBGMEQCIGiZNOV7 +IvcHKU7nEaxFgWPpUu2CxyULg1ueJTYwTT12AiAJWQv3RrqCtOJC7JEdztILs3Bn +an9s0Bf93uOE4C/LiAB3AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw7w5CHrR+Tqo0 +AAABkRUuLxAAAAQDAEgwRgIhAOhlC+IpJV3uAaDCRXi6RZ+V8++QaLaTEtqFp2UP +yWeSAiEA8qtGDk1RE1VGwQQcJCf3CBYU5YTlsZNi7F6hEONLuzMAdwAS8U40vVNy +TIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZEVLi7kAAAEAwBIMEYCIQDWCnSm +N+2/xxo8bl3pbpNBEBZZIwnYPQW0A+SJ0+dboQIhANjH2L0xV/+rPuPMzK40vk3J +1fWHLocLjpgaxGhsBAOzMA0GCSqGSIb3DQEBCwUAA4IBAQBcObVjc1zFdOER50ZF +mI+WyVF8t6nV6dm3zIDraLA4++zKUu9UKNJm9YPqLdPP7uTLHz6wuBNmyuWPdF0r +qAf4vsK3tcAds7kjK8injewEUCPG20mtNMUHyhlNEOJR2ySPPQ6Q+t+TtGAnimKa +Zr86quYgYaJYhoEEXcbB9fMoDQYlJDzgT2DXvfM4cyoden2tYZ3gQS6ftiXacBe0 +WzFWYZ8mIP2Kb+D9tCapB9MVUzu3XJVy3S2FLQEWcWIvjnpad73a0/35i/nro6/k +TSK+MKBEBaNZuHJ8ubCToo1BftnsS8HuEPTNe8W1hyc2YmT9f5YQP6HWB2rxjH42 +OTXh -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB @@ -68,36 +68,4 @@ l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5 yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K 00u/I5sUKUErmgQfky3xxzlIPK1aEn8= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 -MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD -VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE -AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 -MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 -MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO -ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI -s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG -vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ -Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb -IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 -tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E -xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV -icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 -D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ -WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ -5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG -KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg -EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID -ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG -BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t -L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr -BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA -A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ -rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ -/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA -CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F -zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA -vGp4z7h/jnZymQyd/teRCBaho1+V ------END CERTIFICATE----- +-----END CERTIFICATE----- \ No newline at end of file diff --git a/static/certs/key.pem b/static/certs/key.pem index 892d7dfa..42a099a8 100644 --- a/static/certs/key.pem +++ b/static/certs/key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqMVOpViuYgF2X -y6yjmOZXMu8/pjC9nGGZFtF1yXguo2A8mEhgDyaIHiIHdybCVo2bx8Z4+/CiRozu -Y/AfJM+AlXFemhMeBMSb7RVEs5IFx4Zb0qNqdE07VsQvom971r/uZAUs2+JnSih4 -ms/bIXCzSge+99OcUgZyowDxhJYGmUcI8DKlEmz8o8eCq3ocj0GQzlNGkUylG0DQ -2ymG3hJplJLtBU6WqN+RjbCa8MyihSIM1gBimPkIssCDHjhn9HU6l+tBkDHl5Cm7 -nom7hSgNdn6FfERqI3U/mzyHUUMshMp7VJG7vlDJJyfW7D2ykxMkIL+Z2OtjvgNp -WBvPVG+zAgMBAAECggEAAzoWM2Xxdt3DaIcxfPr/YXRGYJ2R22myPzw7uN3ODCXu -EDGoknGwsfBoUsRQLtHqgD0K2h/+XjiAn/bmUzpxpY18oP+PRAikT0e9suTFhjVU -EQk7lSwi8fB7BDAydVWk1ywV6qJsqeqx1vLDsb++xEqvpOl/NwqMs4widQtytymu -4n7/5OJik0wMNwSoBApOdRgX4EeGmbPjZj+U8zu1h+xVGDLSAd9stYsZ7jktAZVc -NIiBmNk+d0Laywq+XdD+t3PrbT/IbvqOlq/tAvMI7mAs3t/g6xYWABR6YzkMa0FV -xywzICEgum/ssilWWgnxlAdmhONC/5UNRtg1QflsaQKBgQDkOVN3uTEFuLXnsvyp -IKSxRXnIOc+1RHJiVAZhMGD3Kjr8tuAfTwHFng6CFV6vwAAhli1zU8UJw7U/9rph -aIzNk02RMAPMWQYk1nfUlQkzniG0ydhzI48yEvULSC6t+KKBaQYvmNu6a6pSh+aj -R08r9EzVNRXI9pV22mC+g5C7zQKBgQC+5/JFg55FFyLBzR0SMKHRj6gR1WC0Vovh -tu69yVpg/8JdXUPr7vmtgk617vLP9yttQ4rmBsjeUCG1jtWFDSI9dgtVqolfK+qX -0bh3fmdgolxmta0B51CWdF57zhBnPSoOSuI+d+C4p3AS5Ay1SfPsOCfGu+mZ6KLf -Ee+jYzFZfwKBgQCM7nGCnxOMqvF5sOehMQ1CgtqfMEP5ddkEq0p9PbjDKIrgf7WK -3+kCNYZUAgpEkVYDZ4+Nhg9I5lfItf2GJV+9mtbtby8JQ3gty1qYJahW/bFmyLYm -87B7hYVYgCyDNeRz8Xzma4hUaCP3bwCXl3NmeyfvCSb4wHyvtk7Dls8LiQKBgFZr -IxXqreOyxG4cjtNkJmx57mgcQomAQBQuPka1dm9Ad9jR1mRgKrArs7vR7iLMTeFJ -WQAmBBn3Bjts7CUtu9k8rYbbCxKFC84sBqg5FUz+UnvANBAPiUCCbx72OiCx5G7R -4TbMB3MvgKFckJAkaQH+rard97JPSCNYuDUrOvS7AoGAPRqzqsY1NuSX4NET/5kX -WNpI0C1Y02SodiZEOJiSd1lZdOs+RzKJv0yGZ4bTGzF5g0pPQzRVh7X/RkqvOooi -AdlKGykSXMNzrdgShNxr/RjC+n9+a4pfZWnW8eMbCJWW0ptjycNRbU/rLwmLSuV8 -SOEKVYljbu9o5nFbg1zU0Ck= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE06Eo2N2lCMY4 +ISJIpCXxRMVcEhMyDNl2kDSRPksUOgnPsVEeUCazCZzP9/bB+XOYpjBFKNjp7LQi +GlTD8eWTzfh1y7L5qFQHtv8WHu3pp2eWRU1Fz8qpMw0YNIZDCYllqPzZn8qWX1Ca +M+oVZF8LIDRT01bqO2hAkBZWbJYdh0FQznwWjQcUAWjieb3+p5ZX/2i8gbWe+J5S +8p1WFfZGMyEj+HVHMUPg+B2YCMcjkH3DdxB1EtWVWiKfIbqTsoBfMxkMibsxaM84 +OSOfVPoU9ckBtxZcEwZz/rANPoVtCwU21kiEptCH9UoB5VBKsS5aLI7kxgRuOYfa +NmwM/aaNAgMBAAECggEAEYK8bzxf96tAq0SzXqAP6heSsV7AS28eN7CbpKJUnp+N +OOePDnHWB46e31exoc82DAoY+EYqiiEvY2tRSD9wi8ZCyQQOz6w8kZUju42T3/ov +Ooy+06upXYU3sIQXv8YM7bjridbv+JHRQ27D8BRGamB6l0yRinQvkbLf8d9mOYkj +P5yYrpMPV/mfgkCir/aBlGOzmI+CuOv7FdF9DIz2OehtPXOzbExuab4xOQ4NQrN9 +TfzWWS798D86e5uDx+Ab0pfege8IJvEBjU5ngZo3aeS/F5i2us+BXImu1P6IrYdb +ekXUo9VJPEHiD02iyLW/gFz3/AsWa3ztinXN0I069wKBgQD7yGPX6LG7MXlXEqL2 +DuCrFsKzVC4z0c/cpPXO8Xb8rGzvHR7Oa0i5Bo7i5wRiVXS87HT25iZmB78yjKnI +bVmWA3tVWInx6fixbZUO7L4D/Q1Ddfin/DiXyNpAhKii0QgpD61P7HJnrfnwUar5 +Vpwd2grnPNCbuILZxAZhtIXRnwKBgQDIH5hmyiIUAvrz+4UpE55ecxTMOkj0+Pgx +79KpSjXfEIk5V7UmCSk1SusQWq8Ri9d6QqPcTptVhxmC/geolp9bCW14JdORbjNv +5+3JfAwgZJtbDP4l3GKf168fLQXzSpWCW3vT1lCBz4x4nNs2EudTdDCn5aUVLGEJ +v15Iz0dQUwKBgHuZh8n55SXrx5FDCNSZwRi796Bo9rVhjhTWtgR87NhlHKTVOsZC +TFToL0Sb+776DHCh81kw6jC0JNv/yWkmpQ/LbcQbzrv/C6KuFLpa5Xy3wMcZJpPw +cSex5dI+TTqAOu1NUNsnS5IyCbw7mx8DsWfGHgweApovHa0hWbClGfwpAoGAfSt9 +6DTfkcK3cilMhX+2236BcKe4ADlFC/7jtW0sOsQeAFbCf/LU6ndchVMjEwdzlA3g +bahg8eLZaxw2cBUdwRQpey+1n83csE7RZOeIsi4bGZ0LzWSF71I5P3eqtBxfXTSZ +Q8tVeYv2YW5CkhTKyWDwGePCGHc0jqM6drHm+e8CgYEA+IkvplnK76zx3M579TpI +M2ffiC2b12FtJMpgnNrk6HZ19o2mhbte/7wf9SHVqsRw4V7/n6rzBJ5wzEHOWre7 +7PrkLgS0mY2SRfMmeT74m59dA8GpvhBUW/Xa4wlDtZkU2iIcDkKnYLjgrTKjlK86 +ER+evzmHeTqYJzuK8Mfq93I= -----END PRIVATE KEY----- From d7ec259e2d62fdc2b135a9debc924214c7759480 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 2 Mar 2025 16:02:04 +0100 Subject: [PATCH 083/776] chore: fix inventorySchema transform for projection --- src/models/inventoryModels/inventoryModel.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 7c7645a5..b21eee02 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1345,11 +1345,15 @@ inventorySchema.set("toJSON", { delete returnedObject.__v; delete returnedObject.accountOwnerId; - const inventoryDatabase = returnedObject as IInventoryDatabase; + const inventoryDatabase = returnedObject as Partial; const inventoryResponse = returnedObject as IInventoryClient; - inventoryResponse.TrainingDate = toMongoDate(inventoryDatabase.TrainingDate); - inventoryResponse.Created = toMongoDate(inventoryDatabase.Created); + if (inventoryDatabase.TrainingDate) { + inventoryResponse.TrainingDate = toMongoDate(inventoryDatabase.TrainingDate); + } + if (inventoryDatabase.Created) { + inventoryResponse.Created = toMongoDate(inventoryDatabase.Created); + } if (inventoryDatabase.GuildId) { inventoryResponse.GuildId = toOid(inventoryDatabase.GuildId); } From 36d12e08c796499e4e7459ebae1b950e2921345f Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 05:46:16 -0800 Subject: [PATCH 084/776] chore: turn guild DojoComponents into a DocumentArray (#1070) and use .id for setDojoComponentMessage Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1070 --- .../api/changeDojoRootController.ts | 6 ++--- src/controllers/api/getGuildDojoController.ts | 6 ++--- ...queueDojoComponentDestructionController.ts | 4 +-- .../api/setDojoComponentMessageController.ts | 2 +- .../api/startDojoRecipeController.ts | 2 +- src/models/guildModel.ts | 25 ++++++++++++++++--- src/services/guildService.ts | 16 +++--------- src/types/guildTypes.ts | 2 +- 8 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/controllers/api/changeDojoRootController.ts b/src/controllers/api/changeDojoRootController.ts index 568bf688..d54e564a 100644 --- a/src/controllers/api/changeDojoRootController.ts +++ b/src/controllers/api/changeDojoRootController.ts @@ -9,7 +9,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the root. const idToNode: Record = {}; - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { idToNode[x._id.toString()] = { component: x, parent: undefined, @@ -18,7 +18,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { }); let oldRoot: INode | undefined; - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { const node = idToNode[x._id.toString()]; if (x.pi) { idToNode[x.pi.toString()].children.push(node); @@ -47,7 +47,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { ); top.children.forEach(x => stack.push(x)); } - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { x._id = idMap[x._id.toString()]; if (x.pi) { x.pi = idMap[x.pi.toString()]; diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index 9d7ed93f..1340902e 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -13,15 +13,15 @@ export const getGuildDojoController: RequestHandler = async (req, res) => { } // Populate dojo info if not present - if (!guild.DojoComponents || guild.DojoComponents.length == 0) { - guild.DojoComponents = [ + if (guild.DojoComponents.length == 0) { + guild.DojoComponents.push([ { _id: new Types.ObjectId(), pf: "/Lotus/Levels/ClanDojo/DojoHall.level", ppf: "", CompletionTime: new Date(Date.now()) } - ]; + ]); await guild.save(); } diff --git a/src/controllers/api/queueDojoComponentDestructionController.ts b/src/controllers/api/queueDojoComponentDestructionController.ts index 7f612896..750f8392 100644 --- a/src/controllers/api/queueDojoComponentDestructionController.ts +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -5,8 +5,8 @@ import { ExportDojoRecipes } from "warframe-public-export-plus"; export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { const guild = await getGuildForRequest(req); const componentId = req.query.componentId as string; - const component = guild.DojoComponents!.splice( - guild.DojoComponents!.findIndex(x => x._id.toString() === componentId), + const component = guild.DojoComponents.splice( + guild.DojoComponents.findIndex(x => x._id.toString() === componentId), 1 )[0]; const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf); diff --git a/src/controllers/api/setDojoComponentMessageController.ts b/src/controllers/api/setDojoComponentMessageController.ts index 714dd7a5..0b75838b 100644 --- a/src/controllers/api/setDojoComponentMessageController.ts +++ b/src/controllers/api/setDojoComponentMessageController.ts @@ -4,7 +4,7 @@ import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; export const setDojoComponentMessageController: RequestHandler = async (req, res) => { const guild = await getGuildForRequest(req); // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the message. - const component = guild.DojoComponents!.find(x => x._id.equals(req.query.componentId as string))!; + const component = guild.DojoComponents.id(req.query.componentId as string)!; const payload = JSON.parse(String(req.body)) as SetDojoComponentMessageRequest; if ("Name" in payload) { component.Name = payload.Name; diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index 6fd2b5a9..b449b2ed 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -20,7 +20,7 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { guild.DojoEnergy += room.energy; } - guild.DojoComponents!.push({ + guild.DojoComponents.push({ _id: new Types.ObjectId(), pf: request.PlacedComponent.pf, ppf: request.PlacedComponent.ppf, diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 0582dc64..781fcd24 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -4,7 +4,7 @@ import { ITechProjectDatabase, ITechProjectClient } from "@/src/types/guildTypes"; -import { model, Schema } from "mongoose"; +import { Document, Model, model, Schema, Types } from "mongoose"; import { typeCountSchema } from "./inventoryModels/inventoryModel"; import { toMongoDate } from "../helpers/inventoryHelpers"; @@ -44,7 +44,7 @@ techProjectSchema.set("toJSON", { const guildSchema = new Schema( { Name: { type: String, required: true }, - DojoComponents: [dojoComponentSchema], + DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, TechProjects: { type: [techProjectSchema], default: undefined } @@ -52,4 +52,23 @@ const guildSchema = new Schema( { id: false } ); -export const Guild = model("Guild", guildSchema); +type GuildDocumentProps = { + DojoComponents: Types.DocumentArray; +}; + +// eslint-disable-next-line @typescript-eslint/ban-types +type GuildModel = Model; + +export const Guild = model("Guild", guildSchema); + +// eslint-disable-next-line @typescript-eslint/ban-types +export type TGuildDatabaseDocument = Document & + Omit< + IGuildDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof GuildDocumentProps + > & + GuildDocumentProps; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 210bc1a6..6c556ff4 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,10 +1,9 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; -import { Guild } from "@/src/models/guildModel"; +import { Guild, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; -import { IDojoClient, IDojoComponentClient, IGuildDatabase } from "@/src/types/guildTypes"; -import { Document, Types } from "mongoose"; +import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; export const getGuildForRequest = async (req: Request): Promise => { @@ -41,7 +40,7 @@ export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): ID DojoRequestStatus: status, DojoComponents: [] }; - guild.DojoComponents!.forEach(dojoComponent => { + guild.DojoComponents.forEach(dojoComponent => { const clientComponent: IDojoComponentClient = { id: toOid(dojoComponent._id), pf: dojoComponent.pf, @@ -62,12 +61,3 @@ export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): ID }); return dojo; }; - -// eslint-disable-next-line @typescript-eslint/ban-types -export type TGuildDatabaseDocument = Document & - IGuildDatabase & - Required<{ - _id: Types.ObjectId; - }> & { - __v: number; - }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 28a51c71..05c60aee 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -8,7 +8,7 @@ export interface IGuild { export interface IGuildDatabase extends IGuild { _id: Types.ObjectId; - DojoComponents?: IDojoComponentDatabase[]; + DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; DojoEnergy: number; TechProjects?: ITechProjectDatabase[]; From b3003b9fb30ec02e9cf76137475912ccf9b74aef Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 05:48:46 -0800 Subject: [PATCH 085/776] feat: resource extractor drones (#1068) Closes #793 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1068 --- config.json.example | 1 + package-lock.json | 8 +- package.json | 2 +- src/controllers/api/dronesController.ts | 139 ++++++++++++++++++- src/controllers/api/sellController.ts | 6 + src/models/inventoryModels/inventoryModel.ts | 19 ++- src/routes/api.ts | 1 + src/services/configService.ts | 1 + src/services/rngService.ts | 20 ++- src/types/inventoryTypes/inventoryTypes.ts | 7 + static/webui/index.html | 4 + static/webui/translations/en.js | 1 + static/webui/translations/ru.js | 1 + 13 files changed, 200 insertions(+), 10 deletions(-) diff --git a/config.json.example b/config.json.example index 04add3df..c6b12971 100644 --- a/config.json.example +++ b/config.json.example @@ -31,6 +31,7 @@ "unlockExilusEverywhere": true, "unlockArcanesEverywhere": true, "noDailyStandingLimits": true, + "instantResourceExtractorDrones": false, "noDojoResearchCosts": true, "noDojoResearchTime": true, "spoofMasteryRank": -1 diff --git a/package-lock.json b/package-lock.json index 8f67ed87..ad2dcbc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.38", + "warframe-public-export-plus": "^0.5.39", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4083,9 +4083,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.38", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.38.tgz", - "integrity": "sha512-yvc86eOmYPSnnU8LzLBhg/lR1AS1RHID24TqFHVcZuOzMYc934NL8Cv7rtllyefWAMyl7iA5x9tyXSuJWbi6CA==" + "version": "0.5.39", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.39.tgz", + "integrity": "sha512-sEGZedtW4I/M2ceoDs6MQ5eHD7sJgv1KRNLt8BWByXLuDa7qTR3Y9px5TGxqt/rBHKGUyPO1LUxu4bDGZi6yXw==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index f7a70d38..aae92ef4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.38", + "warframe-public-export-plus": "^0.5.39", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index bff5086c..eef59c68 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -1,7 +1,140 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { config } from "@/src/services/configService"; +import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomInt, getRandomWeightedReward3 } from "@/src/services/rngService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; +import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus"; -const dronesController: RequestHandler = (_req, res) => { - res.json({}); +export const dronesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + if ("GetActive" in req.query) { + const activeDrones: IActiveDrone[] = []; + for (const drone of inventory.Drones) { + if (drone.DeployTime) { + activeDrones.push({ + DeployTime: toMongoDate(drone.DeployTime), + System: drone.System!, + ItemId: toOid(drone._id), + ItemType: drone.ItemType, + CurrentHP: drone.CurrentHP, + DamageTime: toMongoDate(drone.DamageTime!), + PendingDamage: drone.PendingDamage!, + Resources: [ + { + ItemType: drone.ResourceType!, + BinTotal: drone.ResourceCount!, + StartTime: toMongoDate(drone.DeployTime) + } + ] + }); + } + } + res.json({ + ActiveDrones: activeDrones + }); + } else if ("droneId" in req.query && "systemIndex" in req.query) { + const drone = inventory.Drones.id(req.query.droneId as string)!; + const droneMeta = ExportDrones[drone.ItemType]; + drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date(); + if (drone.RepairStart) { + const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000; + const hpPerMinute = droneMeta.repairRate / 60; + drone.CurrentHP = Math.min(drone.CurrentHP + Math.round(repairMinutes * hpPerMinute), droneMeta.durability); + drone.RepairStart = undefined; + } + drone.System = parseInt(req.query.systemIndex as string); + const system = ExportSystems[drone.System - 1]; + drone.DamageTime = config.instantResourceExtractorDrones + ? new Date() + : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); + drone.PendingDamage = + Math.random() < system.damageChance + ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) + : 0; + const resource = getRandomWeightedReward3(system.resources, droneMeta.probabilities)!; + //logger.debug(`drone rolled`, resource); + drone.ResourceType = "/Lotus/" + resource.StoreItem.substring(18); + const resourceMeta = ExportResources[drone.ResourceType]; + if (resourceMeta.pickupQuantity) { + const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity]; + drone.ResourceCount = 0; + for (let i = 0; i != pickupsToCollect; ++i) { + drone.ResourceCount += getRandomInt( + resourceMeta.pickupQuantity.minValue, + resourceMeta.pickupQuantity.maxValue + ); + } + } else { + drone.ResourceCount = 1; + } + await inventory.save(); + res.json({}); + } else if ("collectDroneId" in req.query) { + const drone = inventory.Drones.id(req.query.collectDroneId as string)!; + + if (new Date() >= drone.DamageTime!) { + drone.CurrentHP -= drone.PendingDamage!; + drone.RepairStart = new Date(); + } + + const inventoryChanges: IInventoryChanges = {}; + if (drone.CurrentHP <= 0) { + inventory.RegularCredits += 100; + inventoryChanges.RegularCredits = 100; + inventory.Drones.pull({ _id: req.query.collectDroneId as string }); + inventoryChanges.RemovedIdItems = [ + { + ItemId: { $oid: req.query.collectDroneId } + } + ]; + } else { + const completionTime = drone.DeployTime!.getTime() + ExportDrones[drone.ItemType].fillRate * 3600_000; + if (Date.now() >= completionTime) { + const miscItemChanges = [ + { + ItemType: drone.ResourceType!, + ItemCount: drone.ResourceCount! + } + ]; + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + drone.DeployTime = undefined; + drone.System = undefined; + drone.DamageTime = undefined; + drone.PendingDamage = undefined; + drone.ResourceType = undefined; + drone.ResourceCount = undefined; + + inventoryChanges.Drones = [drone.toJSON()]; + } + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); + } else { + throw new Error(`drones.php query not handled`); + } }; -export { dronesController }; +interface IActiveDrone { + DeployTime: IMongoDate; + System: number; + ItemId: IOid; + ItemType: string; + CurrentHP: number; + DamageTime: IMongoDate; + PendingDamage: number; + Resources: { + ItemType: string; + BinTotal: number; + StartTime: IMongoDate; + }[]; +} diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index bf3a346f..466bd197 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -86,6 +86,11 @@ export const sellController: RequestHandler = async (req, res) => { inventory.Hoverboards.pull({ _id: sellItem.String }); }); } + if (payload.Items.Drones) { + payload.Items.Drones.forEach(sellItem => { + inventory.Drones.pull({ _id: sellItem.String }); + }); + } if (payload.Items.Consumables) { const consumablesChanges = []; for (const sellItem of payload.Items.Consumables) { @@ -152,6 +157,7 @@ interface ISellRequest { SentinelWeapons?: ISellItem[]; OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; + Drones?: ISellItem[]; }; SellPrice: number; SellCurrency: diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index b21eee02..2aa16bb9 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -336,7 +336,14 @@ const droneSchema = new Schema( { ItemType: String, CurrentHP: Number, - RepairStart: { type: Date, default: undefined } + RepairStart: { type: Date, default: undefined }, + + DeployTime: { type: Date, default: undefined }, + System: Number, + DamageTime: { type: Date, default: undefined }, + PendingDamage: Number, + ResourceType: String, + ResourceCount: Number }, { id: false } ); @@ -347,6 +354,16 @@ droneSchema.set("toJSON", { const db = obj as IDroneDatabase; client.ItemId = toOid(db._id); + if (db.RepairStart) { + client.RepairStart = toMongoDate(db.RepairStart); + } + + delete db.DeployTime; + delete db.System; + delete db.DamageTime; + delete db.PendingDamage; + delete db.ResourceType; + delete db.ResourceCount; delete obj._id; delete obj.__v; diff --git a/src/routes/api.ts b/src/routes/api.ts index af8de31a..c303059d 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -145,6 +145,7 @@ apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); apiRouter.post("/findSessions.php", findSessionsController); diff --git a/src/services/configService.ts b/src/services/configService.ts index 466b062c..83571774 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -57,6 +57,7 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + instantResourceExtractorDrones?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; spoofMasteryRank?: number; diff --git a/src/services/rngService.ts b/src/services/rngService.ts index f519c0d8..a7ce5ce4 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -18,7 +18,7 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { +export const getRandomReward = (pool: T[]): T | undefined => { if (pool.length == 0) return; const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); @@ -71,3 +71,21 @@ export const getRandomWeightedReward2 = ( } return getRandomReward(resultPool); }; + +export const getRandomWeightedReward3 = ( + pool: T[], + weights: Record +): (T & { probability: number }) | undefined => { + const resultPool: (T & { probability: number })[] = []; + const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; + for (const entry of pool) { + ++rarityCounts[entry.Rarity]; + } + for (const entry of pool) { + resultPool.push({ + ...entry, + probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] + }); + } + return getRandomReward(resultPool); +}; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d81491c1..0496a050 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -520,6 +520,13 @@ export interface IDroneDatabase { CurrentHP: number; _id: Types.ObjectId; RepairStart?: Date; + + DeployTime?: Date; + System?: number; + DamageTime?: Date; + PendingDamage?: number; + ResourceType?: string; + ResourceCount?: number; } export interface ITypeXPItem { diff --git a/static/webui/index.html b/static/webui/index.html index 57e35b47..72dd4de9 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -517,6 +517,10 @@
+
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index dd8f5d0b..406a61dd 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -111,6 +111,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchTime: `No Dojo Research Time`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index a8a2e3ac..a5bc1a4c 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -112,6 +112,7 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, From 3442f15c6df985e42696bb353318d22cc9bc0533 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 05:48:54 -0800 Subject: [PATCH 086/776] fix(starter-bat): don't start if build step has failed (#1069) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1069 --- UPDATE AND START SERVER.bat | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index a46472ed..7aab86b3 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -16,9 +16,10 @@ echo Updating dependencies... call npm i call npm run build -call npm run start - -echo SpaceNinjaServer seems to have crashed. +if %errorlevel% == 0 ( + call npm run start + echo SpaceNinjaServer seems to have crashed. +) :a pause > nul goto a From f97bdea447fdeaf08ebaa386443f776ecbead6c5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 12:48:11 -0800 Subject: [PATCH 087/776] fix: send heart of deimos email when quest is given (#1065) Fixes #1061 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1065 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/questService.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/services/questService.ts b/src/services/questService.ts index 84d42f27..3e036a76 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -81,6 +81,18 @@ export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQu logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`); return; } + + if (questKey.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain") { + void createMessage(inventory.accountOwnerId.toString(), [ + { + sndr: "/Lotus/Language/Bosses/Loid", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", + sub: "/Lotus/Language/InfestedMicroplanet/DeimosIntroQuestInboxTitle", + msg: "/Lotus/Language/InfestedMicroplanet/DeimosIntroQuestInboxMessage" + } + ]); + } + const index = inventory.QuestKeys.push(questKey); return inventory.QuestKeys[index - 1].toJSON(); From 77cadc732ca234a584caa78db14d652740b78c5e Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 12:48:24 -0800 Subject: [PATCH 088/776] chore: give baro his entire stock (from TennoCon 2024) (#1067) given that his offers are currently not rotating Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1067 Co-authored-by: Sainan Co-committed-by: Sainan --- .../worldState/worldState.json | 1880 ++++++++++++++++- 1 file changed, 1855 insertions(+), 25 deletions(-) diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index eef85698..9e10a38c 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -730,31 +730,1861 @@ "Character": "Baro'Ki Teel", "Node": "PlutoHUB", "Manifest": [ - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Skins/GaussSentinelSkin", "PrimePrice": 500, "RegularPrice": 425000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/ElectEventPistolMod", "PrimePrice": 300, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponEventSlashDamageMod", "PrimePrice": 375, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponEventSlashDamageMod", "PrimePrice": 375, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/ElectEventShotgunMod", "PrimePrice": 300, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetSkinVoidTrader", "PrimePrice": 120, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileInarosTomb", "PrimePrice": 325, "RegularPrice": 175000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetBlueSkySkinPrimeTrader", "PrimePrice": 210, "RegularPrice": 450000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/BaroInarosMeleeDangle", "PrimePrice": 250, "RegularPrice": 250000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroInarosPolearmSkin", "PrimePrice": 325, "RegularPrice": 250000 }, - { "ItemType": "/Lotus/Types/StoreItems/Boosters/CreditBooster3DayStoreItem", "PrimePrice": 350, "RegularPrice": 75000 }, - { "ItemType": "/Lotus/Types/StoreItems/Packages/VTEosArmourBundle", "PrimePrice": 285, "RegularPrice": 260000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisSonicor", "PrimePrice": 380, "RegularPrice": 175000 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrineerLeverActionRifle/PrismaGrinlokWeapon", "PrimePrice": 500, "RegularPrice": 220000 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpFreezeRay/Vandal/CrpFreezeRayVandalRifle", "PrimePrice": 475, "RegularPrice": 250000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConJ", "PrimePrice": 75, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConA", "PrimePrice": 75, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/NezhaLeverianCape", "PrimePrice": 400, "RegularPrice": 350000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Glass/GaraAlternateSkin", "PrimePrice": 550, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusBasilisk", "PrimePrice": 100, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/JaviExecutionHood", "PrimePrice": 450, "RegularPrice": 450000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/GaussSentinelWings", "PrimePrice": 400, "RegularPrice": 500000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageCorpusExpert", "PrimePrice": 350, "RegularPrice": 140000 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/MummyQuestKeyBlueprint", "PrimePrice": 100, "RegularPrice": 25000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", "PrimePrice": 15, "RegularPrice": 1000 } + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/TennoCon2024GlyphAlt", + "PrimePrice": 15, + "RegularPrice": 1000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/Emotes/Tennocon2024EmoteAlt", + "PrimePrice": 15, + "RegularPrice": 1000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/HeartOfDeimosAlbumCoverPoster", + "PrimePrice": 80, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConC", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConJ", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConH", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionVoltOdonataPrimeBronze", + "PrimePrice": 125, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionVoltOdonataPrimeBronze", + "PrimePrice": 125, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionMagNovaVaultBBronze", + "PrimePrice": 125, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/SolsticeNelumboCape", + "PrimePrice": 325, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/SummerSolstice/SummerSolsticeTwinGrakatas", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/Staff/TnRibbonStaffSkin", + "PrimePrice": 350, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GunBlade/GrnGunBlade/GrnGunblade", + "PrimePrice": 550, + "RegularPrice": 325000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpBFG/Vandal/VandalCrpBFG", + "PrimePrice": 650, + "RegularPrice": 550000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Event/AmbulasEvent/Expert/SecondaryExplosionRadiusModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/Dragon2024BadgeItem", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/PrimedArchwingDamageOnReloadMod", + "PrimePrice": 375, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/PrimedArchwingRifleFireIterationsMod", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageBaruukDoanStyle", + "PrimePrice": 75, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OctaviaBobbleHead", + "PrimePrice": 50, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Skins/GaussSentinelSkin", + "PrimePrice": 500, + "RegularPrice": 425000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrismaLotusVinesSigil", + "PrimePrice": 55, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageExcaliburActionProto", + "PrimePrice": 75, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageIvaraAction", + "PrimePrice": 75, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/HornSkullScarf", + "PrimePrice": 325, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/RhinoDeluxeSigil", + "PrimePrice": 45, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Events/InfQuantaInfestedAladV", + "PrimePrice": 325, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterD", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterB", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterC", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/PrimedWeaponElectricityDamageMod", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/Expert/AvatarShieldMaxModExpert", + "PrimePrice": 350, + "RegularPrice": 225000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterA", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/EventSigilScarletSpear", + "PrimePrice": 45, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrnOrokinRifle/GrnOrokinRifleWeapon", + "PrimePrice": 675, + "RegularPrice": 625000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisNikana", + "PrimePrice": 375, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/GaussSentinelWings", + "PrimePrice": 400, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/GaussSentinelTail", + "PrimePrice": 400, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/GaussSentinelMask", + "PrimePrice": 450, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropGrineerCutter", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/CNY2023EmblemItem", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/WeGameNewYearFreeTigerSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/CNY2022EmblemItem", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Leverian/IvaraLeverianPovisRecordsDecoration", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/HoodDuviriOperator", + "PrimePrice": 550, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpTonfa/CrpPrismaTonfa", + "PrimePrice": 450, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneDuviri", + "PrimePrice": 800, + "RegularPrice": 650000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/AshLevarianTiara", + "PrimePrice": 550, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/BaroEphemeraB", + "PrimePrice": 250, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Warframe/PromoParis", + "PrimePrice": 315, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/ThraxSigil", + "PrimePrice": 50, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Bow/Longbow/PrismaLenz/PrismaLenzWeapon", + "PrimePrice": 575, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Vignettes/Warframes/ArchwingAFItem", + "PrimePrice": 100, + "RegularPrice": 330000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/QuartersWallpapers/LavosAlchemistWallpaper", + "PrimePrice": 275, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/GrendelOrokinDishSet", + "PrimePrice": 110, + "RegularPrice": 130000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerKiteerItemB", + "PrimePrice": 200, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/NezhaEtchingsTablets", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/GaussTowerOfAltraDeco", + "PrimePrice": 110, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroPlanter", + "PrimePrice": 125, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroPedestal", + "PrimePrice": 150, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Leggings/LeggingsNovaEngineer", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/BodySuits/BodySuitNovaEngineer", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Sleeves/SleevesNovaEngineer", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/HoodNovaEngineer", + "PrimePrice": 350, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BekranZaftBucketBroom", + "PrimePrice": 100, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Warfan/TnMoonWarfan/MoonWarfanWeapon", + "PrimePrice": 410, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/MoonWarfanSugatraMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OstronHeadStatue", + "PrimePrice": 125, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/DomsFinalDrink", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Wisp/WispAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Pacifist/BaruukImmortalSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ErraBobbleHead", + "PrimePrice": 75, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OwlOrdisStatue", + "PrimePrice": 350, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TNWVesoBobbleHead", + "PrimePrice": 75, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TNWTeshinBobbleHead", + "PrimePrice": 75, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Peculiars/EvilSpiritMod", + "PrimePrice": 250, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeL", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeC", + "PrimePrice": 350, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeA", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/BaroCape3Scarf", + "PrimePrice": 500, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisTiberon", + "PrimePrice": 315, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/LotusFlowers", + "PrimePrice": 250, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/UmbraPedestal", + "PrimePrice": 0, + "RegularPrice": 1000000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Dragon/ChromaAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Hoverboard/HoverboardStickerBaroB", + "PrimePrice": 75, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisLatronPistol", + "PrimePrice": 400, + "RegularPrice": 215000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponGlaiveOnKillBuffSecondary", + "PrimePrice": 300, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConA", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponGlaiveSecondaryHeadshotKillMod", + "PrimePrice": 300, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConD", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConB", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponIncreaseRadialExplosionModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Archwing/Primary/ArchwingHeavyPistols/Prisma/PrismaArchHeavyPistols", + "PrimePrice": 525, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/TwinSnakesGlyph", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConF", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConE", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponGlaiveOnSixKillsBuffSecondary", + "PrimePrice": 300, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/WeGameNewYearOxSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConG", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponFreezeDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/GarvLatroxPoster", + "PrimePrice": 80, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrnBoomerang/HalikarWraithWeapon", + "PrimePrice": 450, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConI", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorC", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorL", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorA", + "PrimePrice": 315, + "RegularPrice": 215000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponReloadSpeedModExpert", + "PrimePrice": 300, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/PrismaMachete", + "PrimePrice": 400, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MoaPet/BaroMoaPetSkin", + "PrimePrice": 500, + "RegularPrice": 325000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Deimos/PlushySunMonsterCommon", + "PrimePrice": 150, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Deimos/PlushyMoonMonsterCommon", + "PrimePrice": 150, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponClipMaxModExpert", + "PrimePrice": 280, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponClipMaxModExpert", + "PrimePrice": 280, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/TnShinaiSword/TnShinaiSwordSkin", + "PrimePrice": 375, + "RegularPrice": 280000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorL", + "PrimePrice": 275, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorC", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorA", + "PrimePrice": 315, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/DualSword/DualRibbonKamasSkin", + "PrimePrice": 350, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Infestation/NidusAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/ActionFigureDioramas/EmpyreanRegionADiorama", + "PrimePrice": 155, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropGrineerFlak", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropGrineerTaktis", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/AshLeverianLiosPistol", + "PrimePrice": 400, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Glass/GaraAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponSnipersConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/EraHypnosisPoster", + "PrimePrice": 100, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/NezhaLeverianCape", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Leverian/NezhaLeverian/NezhaLeverianPolearm", + "PrimePrice": 350, + "RegularPrice": 325000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BoredTennoPoster", + "PrimePrice": 90, + "RegularPrice": 120000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusBasilisk", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusWeaver", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusHarpi", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Archwing/GrendelArchwingSkin", + "PrimePrice": 400, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/JaviExecutionHood", + "PrimePrice": 450, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/ElectEventMeleeMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/FireEventMeleeMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/ClawCmbTwoMeleeTree", + "PrimePrice": 385, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/FireEventRifleMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/AxeCmbThreeMeleeTree", + "PrimePrice": 385, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/BowMultiShotOnHitMod", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/ElectEventShotgunMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/FireEventPistolMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/FireEventShotgunMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponEventPistolImpactDamageMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/PrimedWeaponCritDamageMod", + "PrimePrice": 400, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageInfestedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageGrineerExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageCorruptedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageCorpusExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponClipMaxModExpert", + "PrimePrice": 280, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/Expert/ArchwingRifleDamageAmountModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponRifleConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPrecepts/PrimedRegen", + "PrimePrice": 300, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeRangeIncModExpert", + "PrimePrice": 300, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponCritDamageModExpert", + "PrimePrice": 280, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponReloadSpeedModExpert", + "PrimePrice": 375, + "RegularPrice": 120000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeDamageModExpert", + "PrimePrice": 385, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponDamageAmountModExpert", + "PrimePrice": 300, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponCritChanceModBeginnerExpert", + "PrimePrice": 400, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Sentinel/Kubrow/Expert/KubrowPackLeaderExpertMod", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Expert/ArchwingSuitAbilityStrengthModExpert", + "PrimePrice": 350, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponImpactDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponFireDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/Expert/AvatarPowerMaxModExpert", + "PrimePrice": 350, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponToxinDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponReloadSpeedModExpert", + "PrimePrice": 375, + "RegularPrice": 120000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageInfestedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageGrineerExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageCorruptedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageCorpusExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponFreezeDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/Expert/AvatarAbilityDurationModExpert", + "PrimePrice": 350, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageInfestedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageGrineerExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageCorruptedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageCorpusExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponElectricityDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageInfested", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageGrineer", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageCorrupted", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageCorpus", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Sentinel/SentinelLootRadarEnemyRadarExpertMod", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/GlaiveCmbTwoMeleeTree", + "PrimePrice": 385, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponEventMeleeImpactDamageMod", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponEventRifleImpactDamageMod", + "PrimePrice": 330, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/WeaponEventShotgunImpactDamageMod", + "PrimePrice": 365, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/ElectEventRifleMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/ElectEventPistolMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/VoidTrader/VTDetron", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpFreezeRay/Vandal/CrpFreezeRayVandalRifle", + "PrimePrice": 475, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/ClanTech/Chemical/FlameThrowerWraith", + "PrimePrice": 550, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/WraithMacheteWeapon", + "PrimePrice": 410, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CrpHandRL/PrismaAngstrum", + "PrimePrice": 475, + "RegularPrice": 210000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/PrismaDualCleavers", + "PrimePrice": 490, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/VoidTraderGorgon/VTGorgon", + "PrimePrice": 600, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/VoidTrader/PrismaGrakata", + "PrimePrice": 610, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrineerLeverActionRifle/PrismaGrinlokWeapon", + "PrimePrice": 500, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Melee/KickAndPunch/PrismaObex", + "PrimePrice": 500, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/VoidTrader/PrismaSkana", + "PrimePrice": 510, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CorpusUMP/PrismaCorpusUMP", + "PrimePrice": 400, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Pistols/GrineerBulbousSMG/Prisma/PrismaTwinGremlinsWeapon", + "PrimePrice": 500, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Archwing/Melee/VoidTraderArchsword/VTArchSwordWeapon", + "PrimePrice": 550, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/ClanTech/Energy/VandalElectroProd", + "PrimePrice": 410, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpShockRifle/QuantaVandal", + "PrimePrice": 450, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/Machinegun/SupraVandal", + "PrimePrice": 500, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Pistols/WraithSingleViper/WraithSingleViper", + "PrimePrice": 400, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrineerSniperRifle/VulkarWraith", + "PrimePrice": 450, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/ConclaveLeverPistol/ConclaveLeverPistol", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/FireMeleeDangle", + "PrimePrice": 100, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroInarosPolearmSkin", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/BaroInarosMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/InfestedMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/VTHalloweenDarkSword", + "PrimePrice": 320, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/SummerSolstice/SummerSolsticeGorgon", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/SummerSolstice/SummerIgnisSkin", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroArrow", + "PrimePrice": 375, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/BaroMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroScytheMacheteSkin", + "PrimePrice": 375, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisOdonataSkin", + "PrimePrice": 350, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisBallasSword", + "PrimePrice": 350, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/PrismaArrow", + "PrimePrice": 350, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/VTRedeemerSkin", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisSonicor", + "PrimePrice": 380, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisTigris", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/VTQuanta", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisOpticor", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Halloween/HalloweenDread", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageBaroKiteer", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageGlyphCookieKavat", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageGlyphCookieKubrow", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/LisetScarf", + "PrimePrice": 600, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronChestArmorElixis", + "PrimePrice": 275, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronLegArmorElixis", + "PrimePrice": 300, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronArmArmorElixis", + "PrimePrice": 325, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerTwitchBItemA", + "PrimePrice": 220, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/VTEosArmourBundle", + "PrimePrice": 285, + "RegularPrice": 260000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosChestArmor", + "PrimePrice": 125, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", + "PrimePrice": 15, + "RegularPrice": 1000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroKavatBadgeItem", + "PrimePrice": 50, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/BaroKavatSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesChestArmor", + "PrimePrice": 300, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/WraithTurbinesScarf", + "PrimePrice": 400, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesLegArmor", + "PrimePrice": 350, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesArmArmor", + "PrimePrice": 350, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Pirate/HydroidAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/Halloween2019GrendelTreat", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourC", + "PrimePrice": 150, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerKiteerItemA", + "PrimePrice": 150, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/KazBaroCape", + "PrimePrice": 325, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/BaroEphemeraA", + "PrimePrice": 100, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoC", + "PrimePrice": 175, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoL", + "PrimePrice": 225, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoA", + "PrimePrice": 310, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourL", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/BaroCape2Scarf", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroQuantumBadgeItem", + "PrimePrice": 400, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourA", + "PrimePrice": 350, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/SolsticeBaroCape", + "PrimePrice": 425, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/BaroCape", + "PrimePrice": 500, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageBaroIcon", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosALArmor", + "PrimePrice": 50, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosLLArmor", + "PrimePrice": 65, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeLegLeftArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeArmLeftArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoLegLeftArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoArmLeftArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Magician/LimboImmortalSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Cowgirl/MesaImmortallSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Harlequin/MirageAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroKubrowBadgeItem", + "PrimePrice": 50, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/BaroKubrowSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisLArmor", + "PrimePrice": 225, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisCArmor", + "PrimePrice": 250, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisAArmor", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeChestArmor", + "PrimePrice": 150, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoChestArmor", + "PrimePrice": 225, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/VTHornSkullScarf", + "PrimePrice": 250, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronChestArmorPrisma", + "PrimePrice": 275, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronLegArmorPrisma", + "PrimePrice": 300, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronArmArmorPrisma", + "PrimePrice": 325, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/PrismaLotusEmblem", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageBaroTwoIcon", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrismaLotusSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/Halloween2014Wings/PrismaNaberusArmArmor", + "PrimePrice": 220, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrimeTraderSigil", + "PrimePrice": 50, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrismaRazorScarf", + "PrimePrice": 350, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/VTDinoSpikeScarf", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageLowPolyKavat", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageLowPolyKubrow", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosARArmor", + "PrimePrice": 50, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosLRArmor", + "PrimePrice": 65, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeLegRightArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeArmRightArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoLegRightArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoArmRightArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Tengu/ZephyrAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/Patterns/KubrowPetPatternPrimeTraderA", + "PrimePrice": 150, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Camo/DesertDirigaSkin", + "PrimePrice": 225, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/KavatPetMask", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/KavatPetTail", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/KavatPetWings", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/CatbrowArmorVoidTraderA", + "PrimePrice": 500, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Kubrows/Armor/KubrowArmorBaro", + "PrimePrice": 500, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/BaroPetMask", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/BaroPetTail", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/BaroPetWings", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/KavatColorPackNexus", + "PrimePrice": 200, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/PrismaJetWings", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/PrismaFishTail", + "PrimePrice": 200, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/PrismaMechHeadMask", + "PrimePrice": 175, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Kubrows/Armor/KubrowArmorPrisma", + "PrimePrice": 400, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPowersuits/PrismaShadePowerSuit", + "PrimePrice": 500, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Skins/DesertTaxonSkin", + "PrimePrice": 200, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/CatbrowArmorHalloweenA", + "PrimePrice": 400, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem", + "PrimePrice": 450, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/CreditBooster3DayStoreItem", + "PrimePrice": 350, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem", + "PrimePrice": 500, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/ResourceAmount3DayStoreItem", + "PrimePrice": 400, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionPBronze", + "PrimePrice": 50, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Recipes/Components/CorruptedBombardBallBlueprint", + "PrimePrice": 100, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/CorruptedHeavyGunnerBall", + "PrimePrice": 100, + "RegularPrice": 40000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OrbiterPictureFrameBaro", + "PrimePrice": 100, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBaitC", + "PrimePrice": 200, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileInarosTomb", + "PrimePrice": 325, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/BaroFireWorksCrate", + "PrimePrice": 50, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileOrokinExtraction", + "PrimePrice": 325, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Keys/MummyQuestKeyBlueprint", + "PrimePrice": 100, + "RegularPrice": 25000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBait", + "PrimePrice": 200, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBaitB", + "PrimePrice": 200, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationB", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationE", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneBaro", + "PrimePrice": 700, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerBobbleHead", + "PrimePrice": 70, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Hoverboard/HoverboardStickerBaroA", + "PrimePrice": 75, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/KavatBust", + "PrimePrice": 220, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/KubrowBust", + "PrimePrice": 220, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDesertSkate", + "PrimePrice": 125, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationD", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ExcaliburArchwingBobbleHead", + "PrimePrice": 90, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroTiara", + "PrimePrice": 525, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/EarpieceBaroC", + "PrimePrice": 500, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroMouthPieceA", + "PrimePrice": 500, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroVisor", + "PrimePrice": 525, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroHorn", + "PrimePrice": 525, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/EarpieceBaroA", + "PrimePrice": 500, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/EarpieceBaroB", + "PrimePrice": 250, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/QuartersWallpapers/BaroWallpaper", + "PrimePrice": 250, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/InarosLisetSkin", + "PrimePrice": 400, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationA", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetInsectSkinInaros", + "PrimePrice": 425, + "RegularPrice": 320000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetInsectSkinPrimeTrader", + "PrimePrice": 230, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ParazonPoster", + "PrimePrice": 100, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/KubrowKavatLowPolyPoster", + "PrimePrice": 90, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetSkinVoidTrader", + "PrimePrice": 120, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetBlueSkySkinPrimeTrader", + "PrimePrice": 210, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationF", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetBlueSkySkinInaros", + "PrimePrice": 375, + "RegularPrice": 340000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationG", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropOstRugBaro", + "PrimePrice": 225, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationH", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/Gyroscope/LisetGyroscopeSkinPrimeTrader", + "PrimePrice": 220, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationC", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/PedistalPrime", + "PrimePrice": 0, + "RegularPrice": 1000000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/Emotes/BaroEmote", + "PrimePrice": 0, + "RegularPrice": 1000000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/EventSniperReloadDamageMod", + "PrimePrice": 2995, + "RegularPrice": 1000000 + } ] } ], From 67a275a0099e32025f9f6666743465cdf8300cb6 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 12:48:39 -0800 Subject: [PATCH 089/776] feat: dojo component "collecting materials" stage (#1071) Closes #1051 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1071 Co-authored-by: Sainan Co-committed-by: Sainan --- config.json.example | 1 + .../api/abortDojoComponentController.ts | 19 +++++ .../contributeToDojoComponentController.ts | 82 +++++++++++++++++++ src/controllers/api/guildTechController.ts | 7 +- .../api/setDojoComponentMessageController.ts | 2 +- .../api/startDojoRecipeController.ts | 24 ++++-- src/models/guildModel.ts | 2 + src/routes/api.ts | 4 + src/services/configService.ts | 1 + src/services/guildService.ts | 49 +++++++---- static/webui/index.html | 4 + static/webui/translations/en.js | 1 + static/webui/translations/ru.js | 1 + 13 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 src/controllers/api/abortDojoComponentController.ts create mode 100644 src/controllers/api/contributeToDojoComponentController.ts diff --git a/config.json.example b/config.json.example index c6b12971..af2e9846 100644 --- a/config.json.example +++ b/config.json.example @@ -32,6 +32,7 @@ "unlockArcanesEverywhere": true, "noDailyStandingLimits": true, "instantResourceExtractorDrones": false, + "noDojoRoomBuildStage": true, "noDojoResearchCosts": true, "noDojoResearchTime": true, "spoofMasteryRank": -1 diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts new file mode 100644 index 00000000..da10a839 --- /dev/null +++ b/src/controllers/api/abortDojoComponentController.ts @@ -0,0 +1,19 @@ +import { getDojoClient, getGuildForRequestEx } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const abortDojoComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest; + // TODO: Move already-contributed credits & items to the clan vault + guild.DojoComponents.pull({ _id: request.ComponentId }); + await guild.save(); + res.json(getDojoClient(guild, 0)); +}; + +export interface IAbortDojoComponentRequest { + ComponentId: string; +} diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts new file mode 100644 index 00000000..72e20ef1 --- /dev/null +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -0,0 +1,82 @@ +import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; +import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; + +export const contributeToDojoComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; + const component = guild.DojoComponents.id(request.ComponentId)!; + const componentMeta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + + component.RegularCredits ??= 0; + if (component.RegularCredits + request.RegularCredits > scaleRequiredCount(componentMeta.price)) { + request.RegularCredits = scaleRequiredCount(componentMeta.price) - component.RegularCredits; + } + component.RegularCredits += request.RegularCredits; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, request.RegularCredits, false); + + component.MiscItems ??= []; + const miscItemChanges: IMiscItem[] = []; + for (const ingredientContribution of request.IngredientContributions) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); + if (componentMiscItem) { + const ingredientMeta = componentMeta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + if ( + componentMiscItem.ItemCount + ingredientContribution.ItemCount > + scaleRequiredCount(ingredientMeta.ItemCount) + ) { + ingredientContribution.ItemCount = + scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + } + componentMiscItem.ItemCount += ingredientContribution.ItemCount; + } else { + component.MiscItems.push(ingredientContribution); + } + miscItemChanges.push({ + ItemType: ingredientContribution.ItemType, + ItemCount: ingredientContribution.ItemCount * -1 + }); + } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + + if (component.RegularCredits >= scaleRequiredCount(componentMeta.price)) { + let fullyFunded = true; + for (const ingredient of componentMeta.ingredients) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); + if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) { + fullyFunded = false; + break; + } + } + if (fullyFunded) { + component.RegularCredits = undefined; + component.MiscItems = undefined; + component.CompletionTime = new Date(Date.now() + componentMeta.time * 1000); + } + } + + await guild.save(); + await inventory.save(); + res.json({ + ...getDojoClient(guild, 0, component._id), + InventoryChanges: inventoryChanges + }); +}; + +export interface IContributeToDojoComponentRequest { + ComponentId: string; + IngredientContributions: { + ItemType: string; + ItemCount: number; + }[]; + RegularCredits: number; + VaultIngredientContributions: []; + VaultCredits: number; +} diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 129131d6..d7cb99ad 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getGuildForRequestEx } from "@/src/services/guildService"; +import { getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; @@ -130,8 +130,3 @@ interface IGuildTechContributeFields { VaultCredits: number; VaultMiscItems: IMiscItem[]; } - -const scaleRequiredCount = (count: number): number => { - // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. - return Math.max(1, Math.trunc(count / 100)); -}; diff --git a/src/controllers/api/setDojoComponentMessageController.ts b/src/controllers/api/setDojoComponentMessageController.ts index 0b75838b..255c4d2e 100644 --- a/src/controllers/api/setDojoComponentMessageController.ts +++ b/src/controllers/api/setDojoComponentMessageController.ts @@ -12,7 +12,7 @@ export const setDojoComponentMessageController: RequestHandler = async (req, res component.Message = payload.Message; } await guild.save(); - res.json(getDojoClient(guild, 1)); + res.json(getDojoClient(guild, 0, component._id)); }; type SetDojoComponentMessageRequest = { Name: string } | { Message: string }; diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index b449b2ed..96a1e99c 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -3,6 +3,7 @@ import { IDojoComponentClient } from "@/src/types/guildTypes"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; interface IStartDojoRecipeRequest { PlacedComponent: IDojoComponentClient; @@ -20,15 +21,20 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { guild.DojoEnergy += room.energy; } - guild.DojoComponents.push({ - _id: new Types.ObjectId(), - pf: request.PlacedComponent.pf, - ppf: request.PlacedComponent.ppf, - pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), - op: request.PlacedComponent.op, - pp: request.PlacedComponent.pp, - CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state. - }); + const component = + guild.DojoComponents[ + guild.DojoComponents.push({ + _id: new Types.ObjectId(), + pf: request.PlacedComponent.pf, + ppf: request.PlacedComponent.ppf, + pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), + op: request.PlacedComponent.op, + pp: request.PlacedComponent.pp + }) - 1 + ]; + if (config.noDojoRoomBuildStage) { + component.CompletionTime = new Date(Date.now()); + } await guild.save(); res.json(getDojoClient(guild, 0)); }; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 781fcd24..417eeb7e 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -16,6 +16,8 @@ const dojoComponentSchema = new Schema({ pp: String, Name: String, Message: String, + RegularCredits: Number, + MiscItems: { type: [typeCountSchema], default: undefined }, CompletionTime: Date }); diff --git a/src/routes/api.ts b/src/routes/api.ts index c303059d..988d5207 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,5 +1,6 @@ import express from "express"; import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController"; +import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -11,6 +12,7 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; @@ -135,6 +137,7 @@ apiRouter.get("/surveys.php", surveysController); apiRouter.get("/updateSession.php", updateSessionGetController); // post +apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); @@ -144,6 +147,7 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); +apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); diff --git a/src/services/configService.ts b/src/services/configService.ts index 83571774..788ee4bf 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -58,6 +58,7 @@ interface IConfig { unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; instantResourceExtractorDrones?: boolean; + noDojoRoomBuildStage?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; spoofMasteryRank?: number; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 6c556ff4..5a4658e1 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -5,6 +5,7 @@ import { Guild, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { Types } from "mongoose"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -27,7 +28,11 @@ export const getGuildForRequestEx = async ( return guild; }; -export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): IDojoClient => { +export const getDojoClient = ( + guild: TGuildDatabaseDocument, + status: number, + componentId: Types.ObjectId | undefined = undefined +): IDojoClient => { const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, Name: guild.Name, @@ -41,23 +46,33 @@ export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): ID DojoComponents: [] }; guild.DojoComponents.forEach(dojoComponent => { - const clientComponent: IDojoComponentClient = { - id: toOid(dojoComponent._id), - pf: dojoComponent.pf, - ppf: dojoComponent.ppf, - Name: dojoComponent.Name, - Message: dojoComponent.Message, - DecoCapacity: 600 - }; - if (dojoComponent.pi) { - clientComponent.pi = toOid(dojoComponent.pi); - clientComponent.op = dojoComponent.op!; - clientComponent.pp = dojoComponent.pp!; + if (!componentId || componentId == dojoComponent._id) { + const clientComponent: IDojoComponentClient = { + id: toOid(dojoComponent._id), + pf: dojoComponent.pf, + ppf: dojoComponent.ppf, + Name: dojoComponent.Name, + Message: dojoComponent.Message, + DecoCapacity: 600 + }; + if (dojoComponent.pi) { + clientComponent.pi = toOid(dojoComponent.pi); + clientComponent.op = dojoComponent.op!; + clientComponent.pp = dojoComponent.pp!; + } + if (dojoComponent.CompletionTime) { + clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + } else { + clientComponent.RegularCredits = dojoComponent.RegularCredits; + clientComponent.MiscItems = dojoComponent.MiscItems; + } + dojo.DojoComponents.push(clientComponent); } - if (dojoComponent.CompletionTime) { - clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); - } - dojo.DojoComponents.push(clientComponent); }); return dojo; }; + +export const scaleRequiredCount = (count: number): number => { + // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. + return Math.max(1, Math.trunc(count / 100)); +}; diff --git a/static/webui/index.html b/static/webui/index.html index 72dd4de9..a84fa719 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -521,6 +521,10 @@
+
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 406a61dd..9294bd29 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -112,6 +112,7 @@ dict = { cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, + cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchTime: `No Dojo Research Time`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index a5bc1a4c..6f3179c6 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -113,6 +113,7 @@ dict = { cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, + cheats_noDojoRoomBuildStage: `[UNTRANSLATED] No Dojo Room Build Stage`, cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, From f3f1bfc89069a178b1c887d00d626189f8d073fb Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 3 Mar 2025 12:48:46 -0800 Subject: [PATCH 090/776] chore: simplify rngService (#1073) getRandomWeightedReward now takes any object with lowercase 'rarity', and the only alternative to it is the 'uc' variant which takes any object with uppercase 'Rarity' usage of IRngResult is now also optional Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1073 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/activateRandomModController.ts | 9 +++--- src/controllers/api/dronesController.ts | 4 +-- src/helpers/relicHelper.ts | 4 +-- src/services/purchaseService.ts | 8 ++--- src/services/rngService.ts | 32 ++++--------------- 5 files changed, 18 insertions(+), 39 deletions(-) diff --git a/src/controllers/api/activateRandomModController.ts b/src/controllers/api/activateRandomModController.ts index 1774e4f7..bdf67212 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -3,7 +3,7 @@ import { IRivenChallenge } from "@/src/helpers/rivenFingerprintHelper"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { addMods, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getRandomElement, getRandomInt, getRandomReward, IRngResult } from "@/src/services/rngService"; +import { getRandomElement, getRandomInt, getRandomReward } from "@/src/services/rngService"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { ExportUpgrades } from "warframe-public-export-plus"; @@ -26,15 +26,14 @@ export const activateRandomModController: RequestHandler = async (req, res) => { Required: getRandomInt(challenge.countRange[0], challenge.countRange[1]) }; if (Math.random() < challenge.complicationChance) { - const complicationsAsRngResults: IRngResult[] = []; + const complications: { type: string; probability: number }[] = []; for (const complication of challenge.complications) { - complicationsAsRngResults.push({ + complications.push({ type: complication.fullName, - itemCount: 1, probability: complication.weight }); } - fingerprintChallenge.Complication = getRandomReward(complicationsAsRngResults)!.type; + fingerprintChallenge.Complication = getRandomReward(complications)!.type; logger.debug( `riven rolled challenge ${fingerprintChallenge.Type} with complication ${fingerprintChallenge.Complication}` ); diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index eef59c68..9337dc62 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -2,7 +2,7 @@ import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { config } from "@/src/services/configService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getRandomInt, getRandomWeightedReward3 } from "@/src/services/rngService"; +import { getRandomInt, getRandomWeightedRewardUc } from "@/src/services/rngService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; @@ -56,7 +56,7 @@ export const dronesController: RequestHandler = async (req, res) => { Math.random() < system.damageChance ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) : 0; - const resource = getRandomWeightedReward3(system.resources, droneMeta.probabilities)!; + const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!; //logger.debug(`drone rolled`, resource); drone.ResourceType = "/Lotus/" + resource.StoreItem.substring(18); const resourceMeta = ExportResources[drone.ResourceType]; diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts index a78c99ec..7c1347d5 100644 --- a/src/helpers/relicHelper.ts +++ b/src/helpers/relicHelper.ts @@ -1,7 +1,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; -import { getRandomWeightedReward2, IRngResult } from "@/src/services/rngService"; +import { getRandomWeightedReward, IRngResult } from "@/src/services/rngService"; import { logger } from "@/src/utils/logger"; import { addMiscItems } from "@/src/services/inventoryService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; @@ -13,7 +13,7 @@ export const crackRelic = async ( const relic = ExportRelics[participant.VoidProjection]; const weights = refinementToWeights[relic.quality]; logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); - const reward = getRandomWeightedReward2( + const reward = getRandomWeightedReward( ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics weights )!; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 2d1c66d3..3a84ec4a 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -8,7 +8,7 @@ import { updateCurrency, updateSlots } from "@/src/services/inventoryService"; -import { getRandomWeightedReward } from "@/src/services/rngService"; +import { getRandomWeightedRewardUc } from "@/src/services/rngService"; import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; @@ -325,14 +325,14 @@ const handleBoosterPackPurchase = async ( }; for (let i = 0; i != quantity; ++i) { for (const weights of pack.rarityWeightsPerRoll) { - const result = getRandomWeightedReward(pack.components, weights); + const result = getRandomWeightedRewardUc(pack.components, weights); if (result) { logger.debug(`booster pack rolled`, result); purchaseResponse.BoosterPackItems += - result.type.split("/Lotus/").join("/Lotus/StoreItems/") + ',{"lvl":0};'; + result.Item.split("/Lotus/").join("/Lotus/StoreItems/") + ',{"lvl":0};'; combineInventoryChanges( purchaseResponse.InventoryChanges, - (await addItem(inventory, result.type, result.itemCount)).InventoryChanges + (await addItem(inventory, result.Item, 1)).InventoryChanges ); } } diff --git a/src/services/rngService.ts b/src/services/rngService.ts index a7ce5ce4..e20e5ce4 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -34,45 +34,25 @@ export const getRandomReward = (pool: T[]): T throw new Error("What the fuck?"); }; -export const getRandomWeightedReward = ( - pool: { Item: string; Rarity: TRarity }[], +export const getRandomWeightedReward = ( + pool: T[], weights: Record -): IRngResult | undefined => { - const resultPool: IRngResult[] = []; - const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; - for (const entry of pool) { - ++rarityCounts[entry.Rarity]; - } - for (const entry of pool) { - resultPool.push({ - type: entry.Item, - itemCount: 1, - probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] - }); - } - return getRandomReward(resultPool); -}; - -export const getRandomWeightedReward2 = ( - pool: { type: string; itemCount: number; rarity: TRarity }[], - weights: Record -): IRngResult | undefined => { - const resultPool: IRngResult[] = []; +): (T & { probability: number }) | undefined => { + const resultPool: (T & { probability: number })[] = []; const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; for (const entry of pool) { ++rarityCounts[entry.rarity]; } for (const entry of pool) { resultPool.push({ - type: entry.type, - itemCount: entry.itemCount, + ...entry, probability: weights[entry.rarity] / rarityCounts[entry.rarity] }); } return getRandomReward(resultPool); }; -export const getRandomWeightedReward3 = ( +export const getRandomWeightedRewardUc = ( pool: T[], weights: Record ): (T & { probability: number }) | undefined => { From 2ec110733f6bfc8791588bfee48e54dbdfd906d6 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 4 Mar 2025 08:34:42 +0100 Subject: [PATCH 091/776] note --- src/controllers/api/contributeToDojoComponentController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 72e20ef1..827475b0 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -10,6 +10,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); const guild = await getGuildForRequestEx(req, inventory); + // Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in. const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; const component = guild.DojoComponents.id(request.ComponentId)!; const componentMeta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; From fba1808b0714cdc0ab08c28f287c17f65a8d97bd Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 4 Mar 2025 19:31:09 +0100 Subject: [PATCH 092/776] fix: failure to create a new dojo --- src/controllers/api/getGuildDojoController.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index 1340902e..dbfa0af9 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -14,14 +14,12 @@ export const getGuildDojoController: RequestHandler = async (req, res) => { // Populate dojo info if not present if (guild.DojoComponents.length == 0) { - guild.DojoComponents.push([ - { - _id: new Types.ObjectId(), - pf: "/Lotus/Levels/ClanDojo/DojoHall.level", - ppf: "", - CompletionTime: new Date(Date.now()) - } - ]); + guild.DojoComponents.push({ + _id: new Types.ObjectId(), + pf: "/Lotus/Levels/ClanDojo/DojoHall.level", + ppf: "", + CompletionTime: new Date(Date.now()) + }); await guild.save(); } From 0869bbfb27083b0b6a49cb7c545f27acef37a407 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 4 Mar 2025 10:33:38 -0800 Subject: [PATCH 093/776] feat: rush dojo component (#1075) Closes #1072 This whole system is a bit weird to me. It seems the RushPlatinum is not used by the client at all, so the server just adjusts the CompletionTime. We seem to be about 1% off, but I'm not quite sure why. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1075 --- .../api/dojoComponentRushController.ts | 36 +++++++++++++++++++ src/routes/api.ts | 2 ++ src/types/guildTypes.ts | 8 ++++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/dojoComponentRushController.ts diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts new file mode 100644 index 00000000..91f5f13e --- /dev/null +++ b/src/controllers/api/dojoComponentRushController.ts @@ -0,0 +1,36 @@ +import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; + +export const dojoComponentRushController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest; + const component = guild.DojoComponents.id(request.ComponentId)!; + const componentMeta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + + const fullPlatinumCost = scaleRequiredCount(componentMeta.skipTimePrice); + const fullDurationSeconds = componentMeta.time; + const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; + component.CompletionTime = new Date( + component.CompletionTime!.getTime() - secondsPerPlatinum * request.Amount * 1000 + ); + const inventoryChanges = updateCurrency(inventory, request.Amount, true); + + await guild.save(); + await inventory.save(); + res.json({ + ...getDojoClient(guild, 0, component._id), + InventoryChanges: inventoryChanges + }); +}; + +interface IDojoComponentRushRequest { + ComponentId: string; + Amount: number; + VaultAmount: number; + AllianceVaultAmount: number; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 988d5207..3437d665 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -16,6 +16,7 @@ import { contributeToDojoComponentController } from "@/src/controllers/api/contr import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; +import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; import { dojoController } from "@/src/controllers/api/dojoController"; import { dronesController } from "@/src/controllers/api/dronesController"; import { endlessXpController } from "@/src/controllers/api/endlessXpController"; @@ -149,6 +150,7 @@ apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 05c60aee..4b909708 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -39,14 +39,20 @@ export interface IDojoComponentClient { RegularCredits?: number; // "Collecting Materials" state: Number of credits that were donated. MiscItems?: IMiscItem[]; // "Collecting Materials" state: Resources that were donated. CompletionTime?: IMongoDate; + RushPlatinum?: number; + DestructionTime?: IMongoDate; DecoCapacity?: number; } export interface IDojoComponentDatabase - extends Omit { + extends Omit< + IDojoComponentClient, + "id" | "pi" | "CompletionTime" | "RushPlatinum" | "DestructionTime" | "DecoCapacity" + > { _id: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; + //DestructionTime?: Date; } export interface ITechProjectClient { From bafc6322c20b324c48dd3968c9c9735d117c2cc0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 5 Mar 2025 03:51:48 -0800 Subject: [PATCH 094/776] fix: proper response for fusionTreasures.php (#1078) Fixes #1077 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1078 --- src/controllers/api/fusionTreasuresController.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/fusionTreasuresController.ts b/src/controllers/api/fusionTreasuresController.ts index fa01ff97..086f017e 100644 --- a/src/controllers/api/fusionTreasuresController.ts +++ b/src/controllers/api/fusionTreasuresController.ts @@ -23,12 +23,11 @@ export const fusionTreasuresController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); const request = JSON.parse(String(req.body)) as IFusionTreasureRequest; + // Swap treasures const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1); const newTreasure = parseFusionTreasure(request.newTreasureName, 1); - - // Swap treasures - addFusionTreasures(inventory, [oldTreasure]); - addFusionTreasures(inventory, [newTreasure]); + const fusionTreasureChanges = [oldTreasure, newTreasure]; + addFusionTreasures(inventory, fusionTreasureChanges); // Remove consumed stars const miscItemChanges: IMiscItem[] = []; @@ -45,5 +44,9 @@ export const fusionTreasuresController: RequestHandler = async (req, res) => { addMiscItems(inventory, miscItemChanges); await inventory.save(); - res.end(); + // The response itself is the inventory changes for this endpoint. + res.json({ + MiscItems: miscItemChanges, + FusionTreasures: fusionTreasureChanges + }); }; From 0a2b2f521884c6df417ce060827092dbd4a31172 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 5 Mar 2025 07:30:19 -0800 Subject: [PATCH 095/776] chore: update URL to avoid needing a redirect (#1084) Because it obviously doesn't bring you anywhere currently and it could be used by a third party unfortunately. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1084 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- scripts/update-translations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-translations.js b/scripts/update-translations.js index 45067b25..568885e6 100644 --- a/scripts/update-translations.js +++ b/scripts/update-translations.js @@ -1,4 +1,4 @@ -// Based on http://209.141.38.3/OpenWF/Translations/src/branch/main/update.php +// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php // Converted via ChatGPT-4o const fs = require("fs"); From 0de0416ba3fda11ff426c12567f45ab783dbdddb Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 5 Mar 2025 16:31:13 +0100 Subject: [PATCH 096/776] chore: remove unused strings --- static/webui/translations/en.js | 2 -- static/webui/translations/ru.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 9294bd29..628abcd7 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -94,8 +94,6 @@ dict = { cheats_skipAllDialogue: `Skip All Dialogue`, cheats_unlockAllScans: `Unlock All Scans`, cheats_unlockAllMissions: `Unlock All Missions`, - cheats_unlockAllQuests: `Unlock All Quests`, - cheats_completeAllQuests: `Complete All Quests`, cheats_infiniteCredits: `Infinite Credits`, cheats_infinitePlatinum: `Infinite Platinum`, cheats_infiniteEndo: `Infinite Endo`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 6f3179c6..d109c972 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -95,8 +95,6 @@ dict = { cheats_skipAllDialogue: `Пропустить все диалоги`, cheats_unlockAllScans: `Разблокировать все сканирования`, cheats_unlockAllMissions: `Разблокировать все миссии`, - cheats_unlockAllQuests: `Разблокировать все квесты`, - cheats_completeAllQuests: `Завершить все квесты`, cheats_infiniteCredits: `Бесконечные кредиты`, cheats_infinitePlatinum: `Бесконечная платина`, cheats_infiniteEndo: `Бесконечное эндо`, From 97b61b51b7f99d066d3b2a188264f6aaf48d9e12 Mon Sep 17 00:00:00 2001 From: Vitruvio Date: Wed, 5 Mar 2025 22:26:00 -0800 Subject: [PATCH 097/776] feat(webui): french translation (#1085) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1085 Co-authored-by: Vitruvio Co-committed-by: Vitruvio --- static/webui/script.js | 2 +- static/webui/translations/fr.js | 135 ++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 static/webui/translations/fr.js diff --git a/static/webui/script.js b/static/webui/script.js index a73097db..1750f511 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -128,7 +128,7 @@ function setActiveLanguage(lang) { document.querySelector("[data-lang=" + lang + "]").classList.add("active"); window.dictPromise = new Promise(resolve => { - const webui_lang = ["en", "ru"].indexOf(lang) == -1 ? "en" : lang; + const webui_lang = ["en", "ru", "fr"].indexOf(lang) == -1 ? "en" : lang; const script = document.createElement("script"); script.src = "/translations/" + webui_lang + ".js"; script.onload = function () { diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js new file mode 100644 index 00000000..678758ab --- /dev/null +++ b/static/webui/translations/fr.js @@ -0,0 +1,135 @@ +dict = { + general_inventoryUpdateNote: `Note : Les changements effectués ici seront appliqués lors de la syncrhonisation. Visiter la navigation appliquera les changements apportés à l'inventaire.`, + general_addButton: `Ajouter`, + general_bulkActions: `Action groupée`, + code_nonValidAuthz: `Informations de connexion invalides`, + code_changeNameConfirm: `Nouveau nom du compte :`, + code_deleteAccountConfirm: `Supprimer |DISPLAYNAME| (|EMAIL|) ? Cette action est irreversible.`, + code_archgun: `Archgun`, + code_melee: `Melee`, + code_pistol: `Pistolet`, + code_rifle: `Fusil`, + code_shotgun: `Fusil à Pompe`, + code_kitgun: `Kitgun`, + code_zaw: `Zaw`, + code_moteAmp: `Amplificateur Faible`, + code_amp: `Amplificateur`, + code_sirocco: `Sirocco`, + code_kDrive: `K-Drive`, + code_legendaryCore: `Coeur Légendaire`, + code_traumaticPeculiar: `Traumatisme Atypique`, + code_starter: `|MOD| (Défectueux)`, + code_badItem: `(Imposteur)`, + code_maxRank: `Rang Max`, + code_rename: `Renommer`, + code_renamePrompt: `Nouveau nom :`, + code_remove: `Retirer`, + code_addItemsConfirm: `Ajouter |COUNT| items à l'inventaire ?`, + code_noEquipmentToRankUp: `No equipment to rank up.`, + code_succAdded: `Ajouté.`, + code_buffsNumber: `Nombre de buffs`, + code_cursesNumber: `Nombre de débuffs`, + code_rerollsNumber: `Nombre de rerolls`, + code_viewStats: `Voir les stats`, + code_rank: `Rang`, + code_count: `Quantité`, + code_focusAllUnlocked: `Les écoles de Focus sont déjà déverrouillées.`, + code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`, + code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, + code_succImport: `Importé.`, + login_description: `Connexion avec les informations de connexion OpenWF.`, + login_emailLabel: `Email`, + login_passwordLabel: `Mot de passe`, + login_loginButton: `Connexion`, + navbar_logout: `Déconnexion`, + navbar_renameAccount: `Renommer le compte`, + navbar_deleteAccount: `Supprimer le compte`, + navbar_inventory: `Inventaire`, + navbar_mods: `Mods`, + navbar_quests: `Quêtes`, + navbar_cheats: `Cheats`, + navbar_import: `Importer`, + inventory_addItems: `Ajouter des items`, + inventory_suits: `Warframes`, + inventory_longGuns: `Armes principales`, + inventory_pistols: `Armes secondaires`, + inventory_melee: `Armes de melee`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Archguns`, + inventory_spaceMelee: `Archmelee`, + inventory_mechSuits: `Necramechs`, + inventory_sentinels: `Sentinelles`, + inventory_sentinelWeapons: `Armes de sentinelles`, + inventory_operatorAmps: `Amplificateurs`, + inventory_hoverboards: `K-Drives`, + inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, + inventory_bulkAddWeapons: `Ajouter les armes manquantes`, + inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, + inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`, + inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`, + inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`, + inventory_bulkRankUpSuits: `Toutes les Warframes rang max`, + inventory_bulkRankUpWeapons: `Toutes les armes rang max`, + inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`, + inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`, + inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`, + inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`, + + currency_RegularCredits: `Crédits`, + currency_PremiumCredits: `Platinum`, + currency_FusionPoints: `Endo`, + currency_PrimeTokens: `Aya Raffiné`, + currency_owned: `|COUNT| possédés.`, + powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`, + powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations.`, + mods_addRiven: `Ajouter un riven`, + mods_fingerprint: `Empreinte`, + mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, + mods_rivens: `Rivens`, + mods_mods: `Mods`, + mods_bulkAddMods: `Ajouter les mods manquants`, + cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez |DISPLAYNAME| à la ligne administratorNames dans le fichier config.json.`, + cheats_server: `Serveur`, + cheats_skipTutorial: `Passer le tutoriel`, + cheats_skipAllDialogue: `Passer les dialogues`, + cheats_unlockAllScans: `Débloquer tous les scans`, + cheats_unlockAllMissions: `Débloquer toutes les missions`, + cheats_unlockAllQuests: `Débloquer toutes les quêtes`, + cheats_completeAllQuests: `Compléter toutes les quêtes`, + cheats_infiniteCredits: `Crédits infinis`, + cheats_infinitePlatinum: `Platinum infini`, + cheats_infiniteEndo: `Endo infini`, + cheats_infiniteRegalAya: `Aya Raffiné infini`, + cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`, + cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`, + cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`, + cheats_unlockAllFlavourItems: `Débloquer tous les Flavor Items`, + cheats_unlockAllSkins: `Débloquer tous les skins`, + cheats_unlockAllCapturaScenes: `Débloquer toutes les scènes captura`, + cheats_universalPolarityEverywhere: `Polarités universelles partout`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `Réacteurs et Catalyseurs partout`, + cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, + cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, + cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, + cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, + cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_noDojoResearchCosts: `Aucun coût de recherche (Dojo)`, + cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, + cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, + cheats_saveSettings: `Sauvegarder les paramètres`, + cheats_account: `Compte`, + cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`, + cheats_helminthUnlockAll: `Helminth niveau max`, + cheats_changeSupportedSyndicate: `Allégeance`, + cheats_changeButton: `Changer`, + cheats_none: `Aucun`, + cheats_quests: `Quêtes`, + cheats_quests_unlockAll: `Débloquer toutes les quêtes`, + cheats_quests_completeAll: `Compléter toutes les quêtes`, + cheats_quests_completeAllUnlocked: `Compléter toutes les quêtes déverrouillées`, + cheats_quests_resetAll: `Réinitialiser toutes les quêtes`, + cheats_quests_giveAll: `Obtenir toutes les quêtes`, + import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire écraseront celles présentes dans la base de données.`, + import_submit: `Soumettre`, + prettier_sucks_ass: `` +}; From c4ab496aa3569ccde5579e43f296923060c5156b Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 5 Mar 2025 23:54:47 -0800 Subject: [PATCH 098/776] feat: dojo decorations (#1079) Closes #525 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1079 --- .../api/abortDojoComponentController.ts | 16 +++- .../contributeToDojoComponentController.ts | 89 +++++++++++++------ .../api/destroyDojoDecoController.ts | 19 ++++ .../api/dojoComponentRushController.ts | 42 ++++++--- src/controllers/api/getGuildDojoController.ts | 3 +- .../api/placeDecoInComponentController.ts | 43 +++++++++ ...queueDojoComponentDestructionController.ts | 15 +--- .../api/startDojoRecipeController.ts | 3 +- src/models/guildModel.ts | 18 +++- src/routes/api.ts | 4 + src/services/guildService.ts | 45 +++++++++- src/types/guildTypes.ts | 31 +++++-- 12 files changed, 260 insertions(+), 68 deletions(-) create mode 100644 src/controllers/api/destroyDojoDecoController.ts create mode 100644 src/controllers/api/placeDecoInComponentController.ts diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts index da10a839..02c69cec 100644 --- a/src/controllers/api/abortDojoComponentController.ts +++ b/src/controllers/api/abortDojoComponentController.ts @@ -1,4 +1,4 @@ -import { getDojoClient, getGuildForRequestEx } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, removeDojoDeco, removeDojoRoom } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; @@ -8,12 +8,20 @@ export const abortDojoComponentController: RequestHandler = async (req, res) => const inventory = await getInventory(accountId); const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest; + // TODO: Move already-contributed credits & items to the clan vault - guild.DojoComponents.pull({ _id: request.ComponentId }); + if (request.DecoId) { + removeDojoDeco(guild, request.ComponentId, request.DecoId); + } else { + removeDojoRoom(guild, request.ComponentId); + } + await guild.save(); - res.json(getDojoClient(guild, 0)); + res.json(getDojoClient(guild, 0, request.ComponentId)); }; -export interface IAbortDojoComponentRequest { +interface IAbortDojoComponentRequest { + DecoType?: string; ComponentId: string; + DecoId?: string; } diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 827475b0..bde6d6ff 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -1,10 +1,26 @@ +import { TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IDojoContributable } from "@/src/types/guildTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoRecipe } from "warframe-public-export-plus"; + +interface IContributeToDojoComponentRequest { + ComponentId: string; + DecoId?: string; + DecoType?: string; + IngredientContributions: { + ItemType: string; + ItemCount: number; + }[]; + RegularCredits: number; + VaultIngredientContributions: []; + VaultCredits: number; +} export const contributeToDojoComponentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -13,21 +29,54 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r // Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in. const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; const component = guild.DojoComponents.id(request.ComponentId)!; - const componentMeta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + const inventoryChanges: IInventoryChanges = {}; + if (!component.CompletionTime) { + // Room is in "Collecting Materials" state + if (request.DecoId) { + throw new Error("attempt to contribute to a deco in an unfinished room?!"); + } + const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + await processContribution(guild, request, inventory, inventoryChanges, meta, component); + } else { + // Room is past "Collecting Materials" + if (request.DecoId) { + const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; + await processContribution(guild, request, inventory, inventoryChanges, meta, deco); + } + } + + await guild.save(); + await inventory.save(); + res.json({ + ...getDojoClient(guild, 0, component._id), + InventoryChanges: inventoryChanges + }); +}; + +const processContribution = async ( + guild: TGuildDatabaseDocument, + request: IContributeToDojoComponentRequest, + inventory: TInventoryDatabaseDocument, + inventoryChanges: IInventoryChanges, + meta: IDojoRecipe, + component: IDojoContributable +): Promise => { component.RegularCredits ??= 0; - if (component.RegularCredits + request.RegularCredits > scaleRequiredCount(componentMeta.price)) { - request.RegularCredits = scaleRequiredCount(componentMeta.price) - component.RegularCredits; + if (component.RegularCredits + request.RegularCredits > scaleRequiredCount(meta.price)) { + request.RegularCredits = scaleRequiredCount(meta.price) - component.RegularCredits; } component.RegularCredits += request.RegularCredits; - const inventoryChanges: IInventoryChanges = updateCurrency(inventory, request.RegularCredits, false); + inventoryChanges.RegularCredits = -request.RegularCredits; + updateCurrency(inventory, request.RegularCredits, false); component.MiscItems ??= []; const miscItemChanges: IMiscItem[] = []; for (const ingredientContribution of request.IngredientContributions) { const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); if (componentMiscItem) { - const ingredientMeta = componentMeta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; if ( componentMiscItem.ItemCount + ingredientContribution.ItemCount > scaleRequiredCount(ingredientMeta.ItemCount) @@ -47,9 +96,9 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r addMiscItems(inventory, miscItemChanges); inventoryChanges.MiscItems = miscItemChanges; - if (component.RegularCredits >= scaleRequiredCount(componentMeta.price)) { + if (component.RegularCredits >= scaleRequiredCount(meta.price)) { let fullyFunded = true; - for (const ingredient of componentMeta.ingredients) { + for (const ingredient of meta.ingredients) { const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) { fullyFunded = false; @@ -57,27 +106,13 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r } } if (fullyFunded) { + if (request.IngredientContributions.length) { + // We've already updated subpaths of MiscItems, we need to allow MongoDB to save this before we remove MiscItems. + await guild.save(); + } component.RegularCredits = undefined; component.MiscItems = undefined; - component.CompletionTime = new Date(Date.now() + componentMeta.time * 1000); + component.CompletionTime = new Date(Date.now() + meta.time * 1000); } } - - await guild.save(); - await inventory.save(); - res.json({ - ...getDojoClient(guild, 0, component._id), - InventoryChanges: inventoryChanges - }); }; - -export interface IContributeToDojoComponentRequest { - ComponentId: string; - IngredientContributions: { - ItemType: string; - ItemCount: number; - }[]; - RegularCredits: number; - VaultIngredientContributions: []; - VaultCredits: number; -} diff --git a/src/controllers/api/destroyDojoDecoController.ts b/src/controllers/api/destroyDojoDecoController.ts new file mode 100644 index 00000000..d6884fe4 --- /dev/null +++ b/src/controllers/api/destroyDojoDecoController.ts @@ -0,0 +1,19 @@ +import { getDojoClient, getGuildForRequest, removeDojoDeco } from "@/src/services/guildService"; +import { RequestHandler } from "express"; + +export const destroyDojoDecoController: RequestHandler = async (req, res) => { + const guild = await getGuildForRequest(req); + const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest; + + removeDojoDeco(guild, request.ComponentId, request.DecoId); + // TODO: The client says this is supposed to refund the resources to the clan vault, so we should probably do that. + + await guild.save(); + res.json(getDojoClient(guild, 0, request.ComponentId)); +}; + +interface IDestroyDojoDecoRequest { + DecoType: string; + ComponentId: string; + DecoId: string; +} diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 91f5f13e..3058f8ef 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -1,8 +1,18 @@ import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IDojoContributable } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoRecipe } from "warframe-public-export-plus"; + +interface IDojoComponentRushRequest { + DecoType?: string; + DecoId?: string; + ComponentId: string; + Amount: number; + VaultAmount: number; + AllianceVaultAmount: number; +} export const dojoComponentRushController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -10,14 +20,16 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest; const component = guild.DojoComponents.id(request.ComponentId)!; - const componentMeta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; - const fullPlatinumCost = scaleRequiredCount(componentMeta.skipTimePrice); - const fullDurationSeconds = componentMeta.time; - const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; - component.CompletionTime = new Date( - component.CompletionTime!.getTime() - secondsPerPlatinum * request.Amount * 1000 - ); + if (request.DecoId) { + const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; + processContribution(deco, meta, request.Amount); + } else { + const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + processContribution(component, meta, request.Amount); + } + const inventoryChanges = updateCurrency(inventory, request.Amount, true); await guild.save(); @@ -28,9 +40,11 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { }); }; -interface IDojoComponentRushRequest { - ComponentId: string; - Amount: number; - VaultAmount: number; - AllianceVaultAmount: number; -} +const processContribution = (component: IDojoContributable, meta: IDojoRecipe, platinumDonated: number): void => { + const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice); + const fullDurationSeconds = meta.time; + const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; + component.CompletionTime = new Date( + component.CompletionTime!.getTime() - secondsPerPlatinum * platinumDonated * 1000 + ); +}; diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index dbfa0af9..560bf045 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -18,7 +18,8 @@ export const getGuildDojoController: RequestHandler = async (req, res) => { _id: new Types.ObjectId(), pf: "/Lotus/Levels/ClanDojo/DojoHall.level", ppf: "", - CompletionTime: new Date(Date.now()) + CompletionTime: new Date(Date.now()), + DecoCapacity: 600 }); await guild.save(); } diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts new file mode 100644 index 00000000..d25ac548 --- /dev/null +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -0,0 +1,43 @@ +import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; + +export const placeDecoInComponentController: RequestHandler = async (req, res) => { + const guild = await getGuildForRequest(req); + const request = JSON.parse(String(req.body)) as IPlaceDecoInComponentRequest; + // At this point, we know that a member of the guild is making this request. Assuming they are allowed to place decorations. + const component = guild.DojoComponents.id(request.ComponentId)!; + + if (component.DecoCapacity === undefined) { + component.DecoCapacity = Object.values(ExportDojoRecipes.rooms).find( + x => x.resultType == component.pf + )!.decoCapacity; + } + + component.Decos ??= []; + component.Decos.push({ + _id: new Types.ObjectId(), + Type: request.Type, + Pos: request.Pos, + Rot: request.Rot, + Name: request.Name + }); + + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type); + if (meta && meta.capacityCost) { + component.DecoCapacity -= meta.capacityCost; + } + + await guild.save(); + res.json(getDojoClient(guild, 0, component._id)); +}; + +interface IPlaceDecoInComponentRequest { + ComponentId: string; + Revision: number; + Type: string; + Pos: number[]; + Rot: number[]; + Name?: string; +} diff --git a/src/controllers/api/queueDojoComponentDestructionController.ts b/src/controllers/api/queueDojoComponentDestructionController.ts index 750f8392..2e30dc25 100644 --- a/src/controllers/api/queueDojoComponentDestructionController.ts +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -1,19 +1,12 @@ -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequest, removeDojoRoom } from "@/src/services/guildService"; import { RequestHandler } from "express"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { const guild = await getGuildForRequest(req); const componentId = req.query.componentId as string; - const component = guild.DojoComponents.splice( - guild.DojoComponents.findIndex(x => x._id.toString() === componentId), - 1 - )[0]; - const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf); - if (room) { - guild.DojoCapacity -= room.capacity; - guild.DojoEnergy -= room.energy; - } + + removeDojoRoom(guild, componentId); + await guild.save(); res.json(getDojoClient(guild, 1)); }; diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index 96a1e99c..0a0dfc66 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -29,7 +29,8 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { ppf: request.PlacedComponent.ppf, pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), op: request.PlacedComponent.op, - pp: request.PlacedComponent.pp + pp: request.PlacedComponent.pp, + DecoCapacity: room?.decoCapacity }) - 1 ]; if (config.noDojoRoomBuildStage) { diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 417eeb7e..b3f47e04 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -2,12 +2,23 @@ import { IGuildDatabase, IDojoComponentDatabase, ITechProjectDatabase, - ITechProjectClient + ITechProjectClient, + IDojoDecoDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { typeCountSchema } from "./inventoryModels/inventoryModel"; import { toMongoDate } from "../helpers/inventoryHelpers"; +const dojoDecoSchema = new Schema({ + Type: String, + Pos: [Number], + Rot: [Number], + Name: String, + RegularCredits: Number, + MiscItems: { type: [typeCountSchema], default: undefined }, + CompletionTime: Date +}); + const dojoComponentSchema = new Schema({ pf: { type: String, required: true }, ppf: String, @@ -18,7 +29,10 @@ const dojoComponentSchema = new Schema({ Message: String, RegularCredits: Number, MiscItems: { type: [typeCountSchema], default: undefined }, - CompletionTime: Date + CompletionTime: Date, + DestructionTime: Date, + Decos: [dojoDecoSchema], + DecoCapacity: Number }); const techProjectSchema = new Schema( diff --git a/src/routes/api.ts b/src/routes/api.ts index 3437d665..72229b15 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -16,6 +16,7 @@ import { contributeToDojoComponentController } from "@/src/controllers/api/contr import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; +import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; import { dojoController } from "@/src/controllers/api/dojoController"; import { dronesController } from "@/src/controllers/api/dronesController"; @@ -59,6 +60,7 @@ import { missionInventoryUpdateController } from "@/src/controllers/api/missionI import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController"; import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; +import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; @@ -150,6 +152,7 @@ apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); @@ -175,6 +178,7 @@ apiRouter.post("/login.php", loginController); apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); apiRouter.post("/nameWeapon.php", nameWeaponController); +apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 5a4658e1..19a273f2 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -6,6 +6,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -31,7 +32,7 @@ export const getGuildForRequestEx = async ( export const getDojoClient = ( guild: TGuildDatabaseDocument, status: number, - componentId: Types.ObjectId | undefined = undefined + componentId: Types.ObjectId | string | undefined = undefined ): IDojoClient => { const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, @@ -46,14 +47,14 @@ export const getDojoClient = ( DojoComponents: [] }; guild.DojoComponents.forEach(dojoComponent => { - if (!componentId || componentId == dojoComponent._id) { + if (!componentId || dojoComponent._id.equals(componentId)) { const clientComponent: IDojoComponentClient = { id: toOid(dojoComponent._id), pf: dojoComponent.pf, ppf: dojoComponent.ppf, Name: dojoComponent.Name, Message: dojoComponent.Message, - DecoCapacity: 600 + DecoCapacity: dojoComponent.DecoCapacity ?? 600 }; if (dojoComponent.pi) { clientComponent.pi = toOid(dojoComponent.pi); @@ -66,6 +67,20 @@ export const getDojoClient = ( clientComponent.RegularCredits = dojoComponent.RegularCredits; clientComponent.MiscItems = dojoComponent.MiscItems; } + if (dojoComponent.Decos) { + clientComponent.Decos = []; + for (const deco of dojoComponent.Decos) { + clientComponent.Decos.push({ + id: toOid(deco._id), + Type: deco.Type, + Pos: deco.Pos, + Rot: deco.Rot, + CompletionTime: deco.CompletionTime ? toMongoDate(deco.CompletionTime) : undefined, + RegularCredits: deco.RegularCredits, + MiscItems: deco.MiscItems + }); + } + } dojo.DojoComponents.push(clientComponent); } }); @@ -76,3 +91,27 @@ export const scaleRequiredCount = (count: number): number => { // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. return Math.max(1, Math.trunc(count / 100)); }; + +export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: string): void => { + const component = guild.DojoComponents.splice( + guild.DojoComponents.findIndex(x => x._id.equals(componentId)), + 1 + )[0]; + const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf); + if (meta) { + guild.DojoCapacity -= meta.capacity; + guild.DojoEnergy -= meta.energy; + } +}; + +export const removeDojoDeco = (guild: TGuildDatabaseDocument, componentId: string, decoId: string): void => { + const component = guild.DojoComponents.id(componentId)!; + const deco = component.Decos!.splice( + component.Decos!.findIndex(x => x._id.equals(decoId)), + 1 + )[0]; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type); + if (meta && meta.capacityCost) { + component.DecoCapacity! += meta.capacityCost; + } +}; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 4b909708..176cfeea 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -41,18 +41,33 @@ export interface IDojoComponentClient { CompletionTime?: IMongoDate; RushPlatinum?: number; DestructionTime?: IMongoDate; + Decos?: IDojoDecoClient[]; DecoCapacity?: number; } export interface IDojoComponentDatabase - extends Omit< - IDojoComponentClient, - "id" | "pi" | "CompletionTime" | "RushPlatinum" | "DestructionTime" | "DecoCapacity" - > { + extends Omit { _id: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; - //DestructionTime?: Date; + DestructionTime?: Date; + Decos?: IDojoDecoDatabase[]; +} + +export interface IDojoDecoClient { + id: IOid; + Type: string; + Pos: number[]; + Rot: number[]; + Name?: string; // for teleporters + RegularCredits?: number; + MiscItems?: IMiscItem[]; + CompletionTime?: IMongoDate; +} + +export interface IDojoDecoDatabase extends Omit { + _id: Types.ObjectId; + CompletionTime?: Date; } export interface ITechProjectClient { @@ -66,3 +81,9 @@ export interface ITechProjectClient { export interface ITechProjectDatabase extends Omit { CompletionDate?: Date; } + +export interface IDojoContributable { + RegularCredits?: number; + MiscItems?: IMiscItem[]; + CompletionTime?: Date; +} From 6daa8ab5da8d4c62a316e35ad4d813363d4c1a01 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 6 Mar 2025 09:29:48 +0100 Subject: [PATCH 099/776] chore(webui): fixup french translation --- static/webui/translations/fr.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 678758ab..45480bc5 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -1,3 +1,4 @@ +// French translation by Vitruvio dict = { general_inventoryUpdateNote: `Note : Les changements effectués ici seront appliqués lors de la syncrhonisation. Visiter la navigation appliquera les changements apportés à l'inventaire.`, general_addButton: `Ajouter`, @@ -94,8 +95,6 @@ dict = { cheats_skipAllDialogue: `Passer les dialogues`, cheats_unlockAllScans: `Débloquer tous les scans`, cheats_unlockAllMissions: `Débloquer toutes les missions`, - cheats_unlockAllQuests: `Débloquer toutes les quêtes`, - cheats_completeAllQuests: `Compléter toutes les quêtes`, cheats_infiniteCredits: `Crédits infinis`, cheats_infinitePlatinum: `Platinum infini`, cheats_infiniteEndo: `Endo infini`, From 77aa1caa8f3717439d650532ac514491276c7dd9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 6 Mar 2025 07:19:01 -0800 Subject: [PATCH 100/776] feat: dojo room destruction stage (#1089) Closes #1074 Based on what I could find, apparently only rooms need 2 hours to destroy and decos are removed instantly. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1089 --- config.json.example | 1 + .../api/abortDojoComponentController.ts | 3 +- ...abortDojoComponentDestructionController.ts | 12 ++++++++ .../api/changeDojoRootController.ts | 2 +- .../contributeToDojoComponentController.ts | 2 +- .../api/destroyDojoDecoController.ts | 3 +- .../api/dojoComponentRushController.ts | 2 +- src/controllers/api/getGuildDojoController.ts | 2 +- .../api/placeDecoInComponentController.ts | 2 +- ...queueDojoComponentDestructionController.ts | 9 ++++-- .../api/setDojoComponentMessageController.ts | 2 +- .../api/startDojoRecipeController.ts | 2 +- src/routes/api.ts | 2 ++ src/services/configService.ts | 1 + src/services/guildService.ts | 30 ++++++++++++++++--- static/webui/index.html | 4 +++ static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + 19 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 src/controllers/api/abortDojoComponentDestructionController.ts diff --git a/config.json.example b/config.json.example index af2e9846..db0f6d41 100644 --- a/config.json.example +++ b/config.json.example @@ -33,6 +33,7 @@ "noDailyStandingLimits": true, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": true, + "fastDojoRoomDestruction": true, "noDojoResearchCosts": true, "noDojoResearchTime": true, "spoofMasteryRank": -1 diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts index 02c69cec..fbeb3670 100644 --- a/src/controllers/api/abortDojoComponentController.ts +++ b/src/controllers/api/abortDojoComponentController.ts @@ -9,7 +9,6 @@ export const abortDojoComponentController: RequestHandler = async (req, res) => const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest; - // TODO: Move already-contributed credits & items to the clan vault if (request.DecoId) { removeDojoDeco(guild, request.ComponentId, request.DecoId); } else { @@ -17,7 +16,7 @@ export const abortDojoComponentController: RequestHandler = async (req, res) => } await guild.save(); - res.json(getDojoClient(guild, 0, request.ComponentId)); + res.json(await getDojoClient(guild, 0, request.ComponentId)); }; interface IAbortDojoComponentRequest { diff --git a/src/controllers/api/abortDojoComponentDestructionController.ts b/src/controllers/api/abortDojoComponentDestructionController.ts new file mode 100644 index 00000000..1df71495 --- /dev/null +++ b/src/controllers/api/abortDojoComponentDestructionController.ts @@ -0,0 +1,12 @@ +import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { RequestHandler } from "express"; + +export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => { + const guild = await getGuildForRequest(req); + const componentId = req.query.componentId as string; + + guild.DojoComponents.id(componentId)!.DestructionTime = undefined; + + await guild.save(); + res.json(await getDojoClient(guild, 0, componentId)); +}; diff --git a/src/controllers/api/changeDojoRootController.ts b/src/controllers/api/changeDojoRootController.ts index d54e564a..d596aae3 100644 --- a/src/controllers/api/changeDojoRootController.ts +++ b/src/controllers/api/changeDojoRootController.ts @@ -58,7 +58,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { await guild.save(); - res.json(getDojoClient(guild, 0)); + res.json(await getDojoClient(guild, 0)); }; interface INode { diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index bde6d6ff..40c6b59f 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -50,7 +50,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r await guild.save(); await inventory.save(); res.json({ - ...getDojoClient(guild, 0, component._id), + ...(await getDojoClient(guild, 0, component._id)), InventoryChanges: inventoryChanges }); }; diff --git a/src/controllers/api/destroyDojoDecoController.ts b/src/controllers/api/destroyDojoDecoController.ts index d6884fe4..1b7ec1dd 100644 --- a/src/controllers/api/destroyDojoDecoController.ts +++ b/src/controllers/api/destroyDojoDecoController.ts @@ -6,10 +6,9 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => { const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest; removeDojoDeco(guild, request.ComponentId, request.DecoId); - // TODO: The client says this is supposed to refund the resources to the clan vault, so we should probably do that. await guild.save(); - res.json(getDojoClient(guild, 0, request.ComponentId)); + res.json(await getDojoClient(guild, 0, request.ComponentId)); }; interface IDestroyDojoDecoRequest { diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 3058f8ef..a19cfa7e 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -35,7 +35,7 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { await guild.save(); await inventory.save(); res.json({ - ...getDojoClient(guild, 0, component._id), + ...(await getDojoClient(guild, 0, component._id)), InventoryChanges: inventoryChanges }); }; diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index 560bf045..04d701be 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -24,5 +24,5 @@ export const getGuildDojoController: RequestHandler = async (req, res) => { await guild.save(); } - res.json(getDojoClient(guild, 0)); + res.json(await getDojoClient(guild, 0)); }; diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index d25ac548..b08a1700 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -30,7 +30,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = } await guild.save(); - res.json(getDojoClient(guild, 0, component._id)); + res.json(await getDojoClient(guild, 0, component._id)); }; interface IPlaceDecoInComponentRequest { diff --git a/src/controllers/api/queueDojoComponentDestructionController.ts b/src/controllers/api/queueDojoComponentDestructionController.ts index 2e30dc25..a4459cdd 100644 --- a/src/controllers/api/queueDojoComponentDestructionController.ts +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -1,12 +1,15 @@ -import { getDojoClient, getGuildForRequest, removeDojoRoom } from "@/src/services/guildService"; +import { config } from "@/src/services/configService"; +import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; import { RequestHandler } from "express"; export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { const guild = await getGuildForRequest(req); const componentId = req.query.componentId as string; - removeDojoRoom(guild, componentId); + guild.DojoComponents.id(componentId)!.DestructionTime = new Date( + Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000) + ); await guild.save(); - res.json(getDojoClient(guild, 1)); + res.json(await getDojoClient(guild, 0, componentId)); }; diff --git a/src/controllers/api/setDojoComponentMessageController.ts b/src/controllers/api/setDojoComponentMessageController.ts index 255c4d2e..92931a54 100644 --- a/src/controllers/api/setDojoComponentMessageController.ts +++ b/src/controllers/api/setDojoComponentMessageController.ts @@ -12,7 +12,7 @@ export const setDojoComponentMessageController: RequestHandler = async (req, res component.Message = payload.Message; } await guild.save(); - res.json(getDojoClient(guild, 0, component._id)); + res.json(await getDojoClient(guild, 0, component._id)); }; type SetDojoComponentMessageRequest = { Name: string } | { Message: string }; diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index 0a0dfc66..30f4ed75 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -37,5 +37,5 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { component.CompletionTime = new Date(Date.now()); } await guild.save(); - res.json(getDojoClient(guild, 0)); + res.json(await getDojoClient(guild, 0)); }; diff --git a/src/routes/api.ts b/src/routes/api.ts index 72229b15..dcc9fddc 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,6 +1,7 @@ import express from "express"; import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController"; import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController"; +import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -105,6 +106,7 @@ const apiRouter = express.Router(); // get apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); +apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); apiRouter.get("/credits.php", creditsController); diff --git a/src/services/configService.ts b/src/services/configService.ts index 788ee4bf..e1c79534 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -59,6 +59,7 @@ interface IConfig { noDailyStandingLimits?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; + fastDojoRoomDestruction?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; spoofMasteryRank?: number; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 19a273f2..bc710c0a 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -7,6 +7,7 @@ import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { logger } from "../utils/logger"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -29,11 +30,11 @@ export const getGuildForRequestEx = async ( return guild; }; -export const getDojoClient = ( +export const getDojoClient = async ( guild: TGuildDatabaseDocument, status: number, componentId: Types.ObjectId | string | undefined = undefined -): IDojoClient => { +): Promise => { const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, Name: guild.Name, @@ -46,6 +47,7 @@ export const getDojoClient = ( DojoRequestStatus: status, DojoComponents: [] }; + const roomsToRemove: Types.ObjectId[] = []; guild.DojoComponents.forEach(dojoComponent => { if (!componentId || dojoComponent._id.equals(componentId)) { const clientComponent: IDojoComponentClient = { @@ -63,6 +65,13 @@ export const getDojoClient = ( } if (dojoComponent.CompletionTime) { clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + if (dojoComponent.DestructionTime) { + if (Date.now() >= dojoComponent.DestructionTime.getTime()) { + roomsToRemove.push(dojoComponent._id); + return; + } + clientComponent.DestructionTime = toMongoDate(dojoComponent.DestructionTime); + } } else { clientComponent.RegularCredits = dojoComponent.RegularCredits; clientComponent.MiscItems = dojoComponent.MiscItems; @@ -84,6 +93,13 @@ export const getDojoClient = ( dojo.DojoComponents.push(clientComponent); } }); + if (roomsToRemove.length) { + logger.debug(`removing now-destroyed rooms`, roomsToRemove); + for (const id of roomsToRemove) { + removeDojoRoom(guild, id); + } + await guild.save(); + } return dojo; }; @@ -92,7 +108,7 @@ export const scaleRequiredCount = (count: number): number => { return Math.max(1, Math.trunc(count / 100)); }; -export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: string): void => { +export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types.ObjectId | string): void => { const component = guild.DojoComponents.splice( guild.DojoComponents.findIndex(x => x._id.equals(componentId)), 1 @@ -102,9 +118,14 @@ export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: strin guild.DojoCapacity -= meta.capacity; guild.DojoEnergy -= meta.energy; } + // TODO: Add resources spent to the clan vault }; -export const removeDojoDeco = (guild: TGuildDatabaseDocument, componentId: string, decoId: string): void => { +export const removeDojoDeco = ( + guild: TGuildDatabaseDocument, + componentId: Types.ObjectId | string, + decoId: Types.ObjectId | string +): void => { const component = guild.DojoComponents.id(componentId)!; const deco = component.Decos!.splice( component.Decos!.findIndex(x => x._id.equals(decoId)), @@ -114,4 +135,5 @@ export const removeDojoDeco = (guild: TGuildDatabaseDocument, componentId: strin if (meta && meta.capacityCost) { component.DecoCapacity! += meta.capacityCost; } + // TODO: Add resources spent to the clan vault }; diff --git a/static/webui/index.html b/static/webui/index.html index a84fa719..77389791 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -525,6 +525,10 @@
+
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 628abcd7..bc1472c0 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -111,6 +111,7 @@ dict = { cheats_noDailyStandingLimits: `No Daily Standing Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchTime: `No Dojo Research Time`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 45480bc5..5acce802 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -112,6 +112,7 @@ dict = { cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `Aucun coût de recherche (Dojo)`, cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index d109c972..96deecc3 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -112,6 +112,7 @@ dict = { cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `[UNTRANSLATED] No Dojo Room Build Stage`, + cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, From 519cb26044f09387855d496740e6889843c1cd65 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:42:30 -0800 Subject: [PATCH 101/776] fix(webui): remove unnecessary elements when changing language (#1095) Fixes #1094 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1095 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/index.html | 5 ++--- static/webui/script.js | 36 +++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/static/webui/index.html b/static/webui/index.html index 77389791..a161bd0c 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -555,9 +555,7 @@
- +
@@ -593,6 +591,7 @@ + diff --git a/static/webui/script.js b/static/webui/script.js index 1750f511..fd0811d6 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -129,7 +129,11 @@ function setActiveLanguage(lang) { window.dictPromise = new Promise(resolve => { const webui_lang = ["en", "ru", "fr"].indexOf(lang) == -1 ? "en" : lang; - const script = document.createElement("script"); + let script = document.getElementById("translations"); + if (script) document.documentElement.removeChild(script); + + script = document.createElement("script"); + script.id = "translations"; script.src = "/translations/" + webui_lang + ".js"; script.onload = function () { updateLocElements(); @@ -157,6 +161,15 @@ function fetchItemList() { req.done(async data => { await dictPromise; + document.querySelectorAll('[id^="datalist-"]').forEach(datalist => { + datalist.innerHTML = ""; + }); + + const syndicateNone = document.createElement("option"); + syndicateNone.setAttribute("data-key", ""); + syndicateNone.value = loc("cheats_none"); + document.getElementById("datalist-Syndicates").appendChild(syndicateNone); + window.archonCrystalUpgrades = data.archonCrystalUpgrades; const itemMap = { @@ -198,15 +211,6 @@ function fetchItemList() { }); } else if (type == "uniqueLevelCaps") { uniqueLevelCaps = items; - } else if (type == "Syndicates") { - items.forEach(item => { - if (item.uniqueName.startsWith("RadioLegion")) item.name += " (" + item.uniqueName + ")"; - const option = document.createElement("option"); - option.value = item.uniqueName; - option.innerHTML = item.name; - document.getElementById("changeSyndicate").appendChild(option); - itemMap[item.uniqueName] = { ...item, type }; - }); } else { items.forEach(item => { if ("badReason" in item) { @@ -216,6 +220,9 @@ function fetchItemList() { item.name += " " + loc("code_badItem"); } } + if (type == "Syndicates" && item.uniqueName.startsWith("RadioLegion")) { + item.name += " (" + item.uniqueName + ")"; + } if (item.uniqueName.substr(0, 18) != "/Lotus/Types/Game/" && item.badReason != "notraw") { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); @@ -564,8 +571,10 @@ function updateInventory() { single.loadRoute("/webui/inventory"); } } - - document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? ""; + document.getElementById("changeSyndicate").value = + [...document.querySelectorAll("#datalist-Syndicates option")].find( + option => option.getAttribute("data-key") === (data.SupportedSyndicate ?? "") + )?.value ?? loc("cheats_none"); }); }); } @@ -1138,7 +1147,8 @@ function doImport() { } function doChangeSupportedSyndicate() { - const uniqueName = document.getElementById("changeSyndicate").value; + const uniqueName = getKey(document.getElementById("changeSyndicate")); + revalidateAuthz(() => { $.get("/api/setSupportedSyndicate.php?" + window.authz + "&syndicate=" + uniqueName).done(function () { updateInventory(); From 95dedaf976d2d2476079664441cf4c748b0b090e Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:43:09 -0800 Subject: [PATCH 102/776] chore(webui): update Russian translation (#1096) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1096 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 96deecc3..3da49d4d 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -19,7 +19,7 @@ dict = { code_kDrive: `К-Драйв`, code_legendaryCore: `Легендарное ядро`, code_traumaticPeculiar: `Травмирующая Странность`, - code_starter: `[UNTRANSLATED] |MOD| (Flawed)`, + code_starter: `|MOD| (Повреждённый)`, code_badItem: `(Самозванец)`, code_maxRank: `Максимальный ранг`, code_rename: `Переименовать`, @@ -99,7 +99,7 @@ dict = { cheats_infinitePlatinum: `Бесконечная платина`, cheats_infiniteEndo: `Бесконечное эндо`, cheats_infiniteRegalAya: `Бесконечная Королевская Айя`, - cheats_infiniteHelminthMaterials: `[UNTRANSLATED] Infinite Helminth Materials`, + cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`, cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`, cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`, cheats_unlockAllFlavourItems: `Разблокировать все уникальные предметы`, @@ -110,11 +110,11 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, - cheats_instantResourceExtractorDrones: `[UNTRANSLATED] Instant Resource Extractor Drones`, - cheats_noDojoRoomBuildStage: `[UNTRANSLATED] No Dojo Room Build Stage`, - cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, - cheats_noDojoResearchCosts: `[UNTRANSLATED] No Dojo Research Costs`, - cheats_noDojoResearchTime: `[UNTRANSLATED] No Dojo Research Time`, + cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, + cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, + cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, + cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, + cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, cheats_saveSettings: `Сохранить настройки`, cheats_account: `Аккаунт`, From 57b3a5b9b3cbb1675f293e3d468c889bd830e2fb Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 6 Mar 2025 21:24:25 -0800 Subject: [PATCH 103/776] feat: clan vault (#1093) Closes #1080 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1093 --- .../contributeToDojoComponentController.ts | 101 +++++++++++------- .../api/contributeToVaultController.ts | 49 +++++++++ .../api/dojoComponentRushController.ts | 2 + src/controllers/api/getGuildController.ts | 4 +- src/controllers/api/guildTechController.ts | 30 +++++- src/models/guildModel.ts | 11 +- src/models/inventoryModels/inventoryModel.ts | 2 +- src/routes/api.ts | 2 + src/services/guildService.ts | 61 +++++++++-- src/types/guildTypes.ts | 38 +++++-- 10 files changed, 238 insertions(+), 62 deletions(-) create mode 100644 src/controllers/api/contributeToVaultController.ts diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 40c6b59f..5aa74855 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -13,12 +13,9 @@ interface IContributeToDojoComponentRequest { ComponentId: string; DecoId?: string; DecoType?: string; - IngredientContributions: { - ItemType: string; - ItemCount: number; - }[]; + IngredientContributions: IMiscItem[]; RegularCredits: number; - VaultIngredientContributions: []; + VaultIngredientContributions: IMiscItem[]; VaultCredits: number; } @@ -37,13 +34,13 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r throw new Error("attempt to contribute to a deco in an unfinished room?!"); } const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; - await processContribution(guild, request, inventory, inventoryChanges, meta, component); + processContribution(guild, request, inventory, inventoryChanges, meta, component); } else { // Room is past "Collecting Materials" if (request.DecoId) { const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; - await processContribution(guild, request, inventory, inventoryChanges, meta, deco); + processContribution(guild, request, inventory, inventoryChanges, meta, deco); } } @@ -55,46 +52,76 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r }); }; -const processContribution = async ( +const processContribution = ( guild: TGuildDatabaseDocument, request: IContributeToDojoComponentRequest, inventory: TInventoryDatabaseDocument, inventoryChanges: IInventoryChanges, meta: IDojoRecipe, component: IDojoContributable -): Promise => { +): void => { component.RegularCredits ??= 0; - if (component.RegularCredits + request.RegularCredits > scaleRequiredCount(meta.price)) { - request.RegularCredits = scaleRequiredCount(meta.price) - component.RegularCredits; + if (request.RegularCredits) { + component.RegularCredits += request.RegularCredits; + inventoryChanges.RegularCredits = -request.RegularCredits; + updateCurrency(inventory, request.RegularCredits, false); + } + if (request.VaultCredits) { + component.RegularCredits += request.VaultCredits; + guild.VaultRegularCredits! -= request.VaultCredits; + } + if (component.RegularCredits > scaleRequiredCount(meta.price)) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price); + component.RegularCredits = scaleRequiredCount(meta.price); } - component.RegularCredits += request.RegularCredits; - inventoryChanges.RegularCredits = -request.RegularCredits; - updateCurrency(inventory, request.RegularCredits, false); component.MiscItems ??= []; - const miscItemChanges: IMiscItem[] = []; - for (const ingredientContribution of request.IngredientContributions) { - const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); - if (componentMiscItem) { - const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; - if ( - componentMiscItem.ItemCount + ingredientContribution.ItemCount > - scaleRequiredCount(ingredientMeta.ItemCount) - ) { - ingredientContribution.ItemCount = - scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + if (request.VaultIngredientContributions.length) { + for (const ingredientContribution of request.VaultIngredientContributions) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); + if (componentMiscItem) { + const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + if ( + componentMiscItem.ItemCount + ingredientContribution.ItemCount > + scaleRequiredCount(ingredientMeta.ItemCount) + ) { + ingredientContribution.ItemCount = + scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + } + componentMiscItem.ItemCount += ingredientContribution.ItemCount; + } else { + component.MiscItems.push(ingredientContribution); } - componentMiscItem.ItemCount += ingredientContribution.ItemCount; - } else { - component.MiscItems.push(ingredientContribution); + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == ingredientContribution.ItemType)!; + vaultMiscItem.ItemCount -= ingredientContribution.ItemCount; } - miscItemChanges.push({ - ItemType: ingredientContribution.ItemType, - ItemCount: ingredientContribution.ItemCount * -1 - }); } - addMiscItems(inventory, miscItemChanges); - inventoryChanges.MiscItems = miscItemChanges; + if (request.IngredientContributions.length) { + const miscItemChanges: IMiscItem[] = []; + for (const ingredientContribution of request.IngredientContributions) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); + if (componentMiscItem) { + const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + if ( + componentMiscItem.ItemCount + ingredientContribution.ItemCount > + scaleRequiredCount(ingredientMeta.ItemCount) + ) { + ingredientContribution.ItemCount = + scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + } + componentMiscItem.ItemCount += ingredientContribution.ItemCount; + } else { + component.MiscItems.push(ingredientContribution); + } + miscItemChanges.push({ + ItemType: ingredientContribution.ItemType, + ItemCount: ingredientContribution.ItemCount * -1 + }); + } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } if (component.RegularCredits >= scaleRequiredCount(meta.price)) { let fullyFunded = true; @@ -106,12 +133,6 @@ const processContribution = async ( } } if (fullyFunded) { - if (request.IngredientContributions.length) { - // We've already updated subpaths of MiscItems, we need to allow MongoDB to save this before we remove MiscItems. - await guild.save(); - } - component.RegularCredits = undefined; - component.MiscItems = undefined; component.CompletionTime = new Date(Date.now() + meta.time * 1000); } } diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts new file mode 100644 index 00000000..dd8b18fa --- /dev/null +++ b/src/controllers/api/contributeToVaultController.ts @@ -0,0 +1,49 @@ +import { getGuildForRequestEx } from "@/src/services/guildService"; +import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const contributeToVaultController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; + + if (request.RegularCredits) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += request.RegularCredits; + } + if (request.MiscItems.length) { + guild.VaultMiscItems ??= []; + for (const item of request.MiscItems) { + guild.VaultMiscItems.push(item); + addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); + } + } + if (request.ShipDecorations.length) { + guild.VaultShipDecorations ??= []; + for (const item of request.ShipDecorations) { + guild.VaultShipDecorations.push(item); + addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); + } + } + if (request.FusionTreasures.length) { + guild.VaultFusionTreasures ??= []; + for (const item of request.FusionTreasures) { + guild.VaultFusionTreasures.push(item); + addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); + } + } + + await guild.save(); + await inventory.save(); + res.end(); +}; + +interface IContributeToVaultRequest { + RegularCredits: number; + MiscItems: IMiscItem[]; + ShipDecorations: ITypeCount[]; + FusionTreasures: IFusionTreasure[]; +} diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index a19cfa7e..f15bce6c 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -47,4 +47,6 @@ const processContribution = (component: IDojoContributable, meta: IDojoRecipe, p component.CompletionTime = new Date( component.CompletionTime!.getTime() - secondsPerPlatinum * platinumDonated * 1000 ); + component.RushPlatinum ??= 0; + component.RushPlatinum += platinumDonated; }; diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index d112f996..6703d87c 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -3,6 +3,7 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Guild } from "@/src/models/guildModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getGuildVault } from "@/src/services/guildService"; const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -62,7 +63,8 @@ const getGuildController: RequestHandler = async (req, res) => { Permissions: 4096 } ], - Tier: 1 + Tier: 1, + Vault: getGuildVault(guild) }); return; } diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index d7cb99ad..a5a080b1 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; +import { getGuildForRequestEx, getGuildVault, scaleRequiredCount } from "@/src/services/guildService"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; @@ -43,10 +43,35 @@ export const guildTechController: RequestHandler = async (req, res) => { } else if (action == "Contribute") { const contributions = data as IGuildTechContributeFields; const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; + + if (contributions.VaultCredits) { + if (contributions.VaultCredits > techProject.ReqCredits) { + contributions.VaultCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= contributions.VaultCredits; + guild.VaultRegularCredits! -= contributions.VaultCredits; + } + if (contributions.RegularCredits > techProject.ReqCredits) { contributions.RegularCredits = techProject.ReqCredits; } techProject.ReqCredits -= contributions.RegularCredits; + + if (contributions.VaultMiscItems.length) { + for (const miscItem of contributions.VaultMiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; + vaultMiscItem.ItemCount -= miscItem.ItemCount; + } + } + } + const miscItemChanges = []; for (const miscItem of contributions.MiscItems) { const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); @@ -74,7 +99,8 @@ export const guildTechController: RequestHandler = async (req, res) => { await guild.save(); await inventory.save(); res.json({ - InventoryChanges: inventoryChanges + InventoryChanges: inventoryChanges, + Vault: getGuildVault(guild) }); } else if (action == "Buy") { const purchase = data as IGuildTechBuyFields; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index b3f47e04..4fdbc223 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -6,7 +6,7 @@ import { IDojoDecoDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; -import { typeCountSchema } from "./inventoryModels/inventoryModel"; +import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; import { toMongoDate } from "../helpers/inventoryHelpers"; const dojoDecoSchema = new Schema({ @@ -16,7 +16,8 @@ const dojoDecoSchema = new Schema({ Name: String, RegularCredits: Number, MiscItems: { type: [typeCountSchema], default: undefined }, - CompletionTime: Date + CompletionTime: Date, + RushPlatinum: Number }); const dojoComponentSchema = new Schema({ @@ -30,6 +31,7 @@ const dojoComponentSchema = new Schema({ RegularCredits: Number, MiscItems: { type: [typeCountSchema], default: undefined }, CompletionTime: Date, + RushPlatinum: Number, DestructionTime: Date, Decos: [dojoDecoSchema], DecoCapacity: Number @@ -63,6 +65,11 @@ const guildSchema = new Schema( DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, + VaultRegularCredits: Number, + VaultPremiumCredits: Number, + VaultMiscItems: { type: [typeCountSchema], default: undefined }, + VaultShipDecorations: { type: [typeCountSchema], default: undefined }, + VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined }, TechProjects: { type: [techProjectSchema], default: undefined } }, { id: false } diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 2aa16bb9..04c2aced 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -552,7 +552,7 @@ questKeysSchema.set("toJSON", { } }); -const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); +export const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); const spectreLoadoutsSchema = new Schema( { diff --git a/src/routes/api.ts b/src/routes/api.ts index dcc9fddc..2ad923d6 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -14,6 +14,7 @@ import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/cla import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; +import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; @@ -153,6 +154,7 @@ apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); +apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index bc710c0a..cd877835 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -3,7 +3,13 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; import { Guild, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; -import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; +import { + IDojoClient, + IDojoComponentClient, + IDojoContributable, + IDojoDecoClient, + IGuildVault +} from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; @@ -30,6 +36,16 @@ export const getGuildForRequestEx = async ( return guild; }; +export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => { + return { + DojoRefundRegularCredits: guild.VaultRegularCredits, + DojoRefundMiscItems: guild.VaultMiscItems, + DojoRefundPremiumCredits: guild.VaultPremiumCredits, + ShipDecorations: guild.VaultShipDecorations, + FusionTreasures: guild.VaultFusionTreasures + }; +}; + export const getDojoClient = async ( guild: TGuildDatabaseDocument, status: number, @@ -41,6 +57,7 @@ export const getDojoClient = async ( Tier: 1, FixedContributions: true, DojoRevision: 1, + Vault: getGuildVault(guild), RevisionTime: Math.round(Date.now() / 1000), Energy: guild.DojoEnergy, Capacity: guild.DojoCapacity, @@ -79,15 +96,20 @@ export const getDojoClient = async ( if (dojoComponent.Decos) { clientComponent.Decos = []; for (const deco of dojoComponent.Decos) { - clientComponent.Decos.push({ + const clientDeco: IDojoDecoClient = { id: toOid(deco._id), Type: deco.Type, Pos: deco.Pos, Rot: deco.Rot, - CompletionTime: deco.CompletionTime ? toMongoDate(deco.CompletionTime) : undefined, - RegularCredits: deco.RegularCredits, - MiscItems: deco.MiscItems - }); + Name: deco.Name + }; + if (deco.CompletionTime) { + clientDeco.CompletionTime = toMongoDate(deco.CompletionTime); + } else { + clientDeco.RegularCredits = deco.RegularCredits; + clientDeco.MiscItems = deco.MiscItems; + } + clientComponent.Decos.push(clientDeco); } } dojo.DojoComponents.push(clientComponent); @@ -118,7 +140,8 @@ export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types guild.DojoCapacity -= meta.capacity; guild.DojoEnergy -= meta.energy; } - // TODO: Add resources spent to the clan vault + moveResourcesToVault(guild, component); + component.Decos?.forEach(deco => moveResourcesToVault(guild, deco)); }; export const removeDojoDeco = ( @@ -135,5 +158,27 @@ export const removeDojoDeco = ( if (meta && meta.capacityCost) { component.DecoCapacity! += meta.capacityCost; } - // TODO: Add resources spent to the clan vault + moveResourcesToVault(guild, deco); +}; + +const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => { + if (component.RegularCredits) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += component.RegularCredits; + } + if (component.MiscItems) { + guild.VaultMiscItems ??= []; + for (const componentMiscItem of component.MiscItems) { + const vaultMiscItem = guild.VaultMiscItems.find(x => x.ItemType == componentMiscItem.ItemType); + if (vaultMiscItem) { + vaultMiscItem.ItemCount += componentMiscItem.ItemCount; + } else { + guild.VaultMiscItems.push(componentMiscItem); + } + } + } + if (component.RushPlatinum) { + guild.VaultPremiumCredits ??= 0; + guild.VaultPremiumCredits += component.RushPlatinum; + } }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 176cfeea..4802070f 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -1,6 +1,6 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "@/src/types/commonTypes"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; export interface IGuild { Name: string; @@ -11,19 +11,38 @@ export interface IGuildDatabase extends IGuild { DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; DojoEnergy: number; + VaultRegularCredits?: number; + VaultPremiumCredits?: number; + VaultMiscItems?: IMiscItem[]; + VaultShipDecorations?: ITypeCount[]; + VaultFusionTreasures?: IFusionTreasure[]; TechProjects?: ITechProjectDatabase[]; } +export interface IGuildVault { + DojoRefundRegularCredits?: number; + DojoRefundMiscItems?: IMiscItem[]; + DojoRefundPremiumCredits?: number; + ShipDecorations?: ITypeCount[]; + FusionTreasures?: IFusionTreasure[]; + DecoRecipes?: ITypeCount[]; // Event Trophies +} + export interface IDojoClient { _id: IOid; // ID of the guild Name: string; Tier: number; FixedContributions: boolean; DojoRevision: number; + AllianceId?: IOid; + Vault?: IGuildVault; + Class?: number; // Level RevisionTime: number; Energy: number; Capacity: number; DojoRequestStatus: number; + ContentURL?: string; + GuildEmblem?: boolean; DojoComponents: IDojoComponentClient[]; } @@ -46,7 +65,7 @@ export interface IDojoComponentClient { } export interface IDojoComponentDatabase - extends Omit { + extends Omit { _id: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; @@ -63,6 +82,7 @@ export interface IDojoDecoClient { RegularCredits?: number; MiscItems?: IMiscItem[]; CompletionTime?: IMongoDate; + RushPlatinum?: number; } export interface IDojoDecoDatabase extends Omit { @@ -70,6 +90,14 @@ export interface IDojoDecoDatabase extends Omit { CompletionDate?: Date; } - -export interface IDojoContributable { - RegularCredits?: number; - MiscItems?: IMiscItem[]; - CompletionTime?: Date; -} From b8b1c5e008d6e8fd749456deed318251d1e3c157 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 7 Mar 2025 00:40:22 -0800 Subject: [PATCH 104/776] feat: library personal target progress (#1083) Closes #1081 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1083 --- .../startLibraryPersonalTargetController.ts | 2 +- src/models/inventoryModels/inventoryModel.ts | 14 +++- src/services/missionInventoryUpdateService.ts | 73 ++++++++++++++++--- src/types/inventoryTypes/inventoryTypes.ts | 2 +- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/startLibraryPersonalTargetController.ts b/src/controllers/api/startLibraryPersonalTargetController.ts index 388dc897..7bfa5ff6 100644 --- a/src/controllers/api/startLibraryPersonalTargetController.ts +++ b/src/controllers/api/startLibraryPersonalTargetController.ts @@ -8,7 +8,7 @@ export const startLibraryPersonalTargetController: RequestHandler = async (req, inventory.LibraryPersonalTarget = req.query.target as string; await inventory.save(); res.json({ - IsQuest: false, + IsQuest: req.query.target == "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget", Target: req.query.target }); }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 04c2aced..49634ca0 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -74,7 +74,8 @@ import { IAlignment, ICollectibleEntry, IIncentiveState, - ISongChallenge + ISongChallenge, + ILibraryPersonalProgress } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1005,6 +1006,15 @@ pendingCouponSchema.set("toJSON", { } }); +const libraryPersonalProgressSchema = new Schema( + { + TargetType: String, + Scans: Number, + Completed: Boolean + }, + { _id: false } +); + const libraryDailyTaskInfoSchema = new Schema( { EnemyTypes: [String], @@ -1271,7 +1281,7 @@ const inventorySchema = new Schema( LibraryPersonalTarget: String, //Cephalon Simaris Entries Example:"TargetType"+"Scans"(1-10)+"Completed": true|false - LibraryPersonalProgress: [Schema.Types.Mixed], + LibraryPersonalProgress: { type: [libraryPersonalProgressSchema], default: [] }, //Cephalon Simaris Daily Task LibraryAvailableDailyTaskInfo: libraryDailyTaskInfoSchema, LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 64814e4b..214d334e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -191,17 +191,48 @@ export const addMissionInventoryUpdates = ( break; case "LibraryScans": value.forEach(scan => { - if (inventory.LibraryActiveDailyTaskInfo) { - if (inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType)) { - inventory.LibraryActiveDailyTaskInfo.Scans ??= 0; - inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count; - } else { - logger.warn( - `ignoring synthesis of ${scan.EnemyType} as it's not part of the active daily task` - ); + let synthesisIgnored = true; + if ( + inventory.LibraryPersonalTarget && + libraryPersonalTargetToAvatar[inventory.LibraryPersonalTarget] == scan.EnemyType + ) { + let progress = inventory.LibraryPersonalProgress.find( + x => x.TargetType == inventory.LibraryPersonalTarget + ); + if (!progress) { + progress = + inventory.LibraryPersonalProgress[ + inventory.LibraryPersonalProgress.push({ + TargetType: inventory.LibraryPersonalTarget, + Scans: 0, + Completed: false + }) - 1 + ]; } - } else { - logger.warn(`no library daily task active, ignoring synthesis of ${scan.EnemyType}`); + progress.Scans += scan.Count; + if ( + progress.Scans >= + (inventory.LibraryPersonalTarget == + "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget" + ? 3 + : 10) + ) { + progress.Completed = true; + } + logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`); + synthesisIgnored = false; + } + if ( + inventory.LibraryActiveDailyTaskInfo && + inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType) + ) { + inventory.LibraryActiveDailyTaskInfo.Scans ??= 0; + inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count; + logger.debug(`synthesis of ${scan.EnemyType} added to daily task progress`); + synthesisIgnored = false; + } + if (synthesisIgnored) { + logger.warn(`ignoring synthesis of ${scan.EnemyType} due to not knowing why you did that`); } }); break; @@ -534,3 +565,25 @@ const corruptedMods = [ "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedReloadSpeedMaxClipRifle", // Depleted Reload "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/FixedShieldAndShieldGatingDuration" // Catalyzing Shields ]; + +const libraryPersonalTargetToAvatar: Record = { + "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget": + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Game/Library/Targets/Research1Target": + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Game/Library/Targets/Research2Target": + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar", + "/Lotus/Types/Game/Library/Targets/Research3Target": + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Game/Library/Targets/Research4Target": "/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar", + "/Lotus/Types/Game/Library/Targets/Research5Target": + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar", + "/Lotus/Types/Game/Library/Targets/Research6Target": "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar", + "/Lotus/Types/Game/Library/Targets/Research7Target": + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar", + "/Lotus/Types/Game/Library/Targets/Research8Target": "/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar", + "/Lotus/Types/Game/Library/Targets/Research9Target": + "/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar", + "/Lotus/Types/Game/Library/Targets/Research10Target": + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" +}; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 0496a050..22f80b99 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -313,7 +313,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Quests: any[]; Robotics: any[]; UsedDailyDeals: any[]; - LibraryPersonalTarget: string; + LibraryPersonalTarget?: string; LibraryPersonalProgress: ILibraryPersonalProgress[]; CollectibleSeries?: ICollectibleEntry[]; LibraryAvailableDailyTaskInfo?: ILibraryDailyTaskInfo; From 530713ce5c7ac0d9095ff255612376ac990a8bf9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 7 Mar 2025 00:40:52 -0800 Subject: [PATCH 105/776] chore: enable "incremental" in tsconfig (#1082) This should make subsequent runs of `npm run build` a bit faster. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1082 --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index c2603f08..7e12112f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ From 59fd816b0c5bc85bddc00da17057aa1460822ffc Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 7 Mar 2025 00:41:18 -0800 Subject: [PATCH 106/776] feat: handle EmailItems received during mission (#1088) Closes #1087 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1088 --- .../api/missionInventoryUpdateController.ts | 2 +- src/services/inventoryService.ts | 26 ++++++++++++++++--- src/services/missionInventoryUpdateService.ts | 25 +++++++++++------- src/types/purchaseTypes.ts | 10 ++++++- src/types/requestTypes.ts | 1 + 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 39bf8180..0dee93ee 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -54,7 +54,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) logger.debug("mission report:", missionReport); const inventory = await getInventory(accountId); - const inventoryUpdates = addMissionInventoryUpdates(inventory, missionReport); + const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); if (missionReport.MissionStatus !== "GS_SUCCESS") { await inventory.save(); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 398cf10c..19fda5e9 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -428,10 +428,8 @@ export const addItem = async ( }; } if (typeName in ExportEmailItems) { - const emailItem = ExportEmailItems[typeName]; - await createMessage(inventory.accountOwnerId.toString(), [convertInboxMessage(emailItem.message)]); return { - InventoryChanges: {} + InventoryChanges: await addEmailItem(inventory, typeName) }; } @@ -943,6 +941,28 @@ const addDrone = ( return inventoryChanges; }; +export const addEmailItem = async ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): Promise => { + const meta = ExportEmailItems[typeName]; + const emailItem = inventory.EmailItems.find(x => x.ItemType == typeName); + if (!emailItem || !meta.sendOnlyOnce) { + await createMessage(inventory.accountOwnerId.toString(), [convertInboxMessage(meta.message)]); + + if (emailItem) { + emailItem.ItemCount += 1; + } else { + inventory.EmailItems.push({ ItemType: typeName, ItemCount: 1 }); + } + + inventoryChanges.EmailItems ??= []; + inventoryChanges.EmailItems.push({ ItemType: typeName, ItemCount: 1 }); + } + return inventoryChanges; +}; + //TODO: wrong id is not erroring export const addGearExpByCategory = ( inventory: TInventoryDatabaseDocument, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 214d334e..45cb1e6a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -14,6 +14,7 @@ import { addConsumables, addCrewShipAmmo, addCrewShipRawSalvage, + addEmailItem, addFocusXpIncreases, addFusionTreasures, addGearExpByCategory, @@ -61,10 +62,10 @@ const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { //type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number]; //const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys -export const addMissionInventoryUpdates = ( +export const addMissionInventoryUpdates = async ( inventory: HydratedDocument, inventoryUpdates: IMissionInventoryUpdateRequest -): Partial | undefined => { +): Promise | undefined> => { //TODO: type this properly const inventoryChanges: Partial = {}; if (inventoryUpdates.MissionFailed === true) { @@ -156,6 +157,12 @@ export const addMissionInventoryUpdates = ( inventoryChanges.FusionPoints = fusionPoints; break; } + case "EmailItems": { + for (const tc of value) { + await addEmailItem(inventory, tc.ItemType); + } + break; + } case "FocusXpIncreases": { addFocusXpIncreases(inventory, value); break; @@ -237,32 +244,32 @@ export const addMissionInventoryUpdates = ( }); break; case "CollectibleScans": - value.forEach(scan => { + for (const scan of value) { const entry = inventory.CollectibleSeries?.find(x => x.CollectibleType == scan.CollectibleType); if (entry) { entry.Count = scan.Count; entry.Tracking = scan.Tracking; if (entry.CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne") { const progress = entry.Count / entry.ReqScans; - entry.IncentiveStates.forEach(gate => { + for (const gate of entry.IncentiveStates) { gate.complete = progress >= gate.threshold; if (gate.complete && !gate.sent) { gate.sent = true; if (gate.threshold == 0.5) { - void createMessage(inventory.accountOwnerId.toString(), [kuriaMessage50]); + await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage50]); } else { - void createMessage(inventory.accountOwnerId.toString(), [kuriaMessage75]); + await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage75]); } } - }); + } if (progress >= 1.0) { - void createMessage(inventory.accountOwnerId.toString(), [kuriaMessage100]); + await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage100]); } } } else { logger.warn(`${scan.CollectibleType} was not found in inventory, ignoring scans`); } - }); + } break; case "Upgrades": value.forEach(clientUpgrade => { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index e921d136..a280787f 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -1,5 +1,11 @@ import { IEquipmentClient } from "./inventoryTypes/commonInventoryTypes"; -import { IDroneClient, IInfestedFoundryClient, IMiscItem, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; +import { + IDroneClient, + IInfestedFoundryClient, + IMiscItem, + ITypeCount, + TEquipmentKey +} from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { PurchaseParams: IPurchaseParams; @@ -33,6 +39,7 @@ export type IInventoryChanges = { InfestedFoundry?: IInfestedFoundryClient; Drones?: IDroneClient[]; MiscItems?: IMiscItem[]; + EmailItems?: ITypeCount[]; } & Record< Exclude< string, @@ -44,6 +51,7 @@ export type IInventoryChanges = { | "InfestedFoundry" | "Drones" | "MiscItems" + | "EmailItems" >, number | object[] >; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 2584bd77..96ac7619 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -46,6 +46,7 @@ export type IMissionInventoryUpdateRequest = { CrewShipRawSalvage?: ITypeCount[]; CrewShipAmmo?: ITypeCount[]; BonusMiscItems?: ITypeCount[]; + EmailItems?: ITypeCount[]; SyndicateId?: string; SortieId?: string; From e4a3b1316046eb0e5547cc07a0617a932b54a8e9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 7 Mar 2025 00:41:36 -0800 Subject: [PATCH 107/776] chore: simplify config (#1090) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1090 --- config.json.example | 4 +--- src/controllers/api/loginController.ts | 4 ++-- src/services/configService.ts | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/config.json.example b/config.json.example index db0f6d41..e710f503 100644 --- a/config.json.example +++ b/config.json.example @@ -5,11 +5,9 @@ "level": "trace" }, "myAddress": "localhost", - "hubAddress": "https://localhost/api/", - "platformCDNs": ["https://localhost/"], - "NRS": ["localhost"], "httpPort": 80, "httpsPort": 443, + "NRS": ["localhost"], "administratorNames": [], "autoCreateAccount": true, "skipTutorial": true, diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index d57fd7e1..5293641f 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -85,8 +85,8 @@ const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): Nonce: account.Nonce, Groups: [], IRC: config.myIrcAddresses ?? [config.myAddress], - platformCDNs: config.platformCDNs, - HUB: config.hubAddress, + platformCDNs: [`https://${config.myAddress}/`], + HUB: `https://${config.myAddress}/api/`, NRS: config.NRS, DTLS: 99, BuildLabel: buildLabel, diff --git a/src/services/configService.ts b/src/services/configService.ts index e1c79534..c17066c0 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -33,8 +33,6 @@ interface IConfig { httpPort?: number; httpsPort?: number; myIrcAddresses?: string[]; - platformCDNs?: string[]; - hubAddress?: string; NRS?: string[]; administratorNames?: string[] | string; autoCreateAccount?: boolean; From 1ad26db331387d190faeb3b495223dc3267ec3b4 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 7 Mar 2025 15:19:39 +0100 Subject: [PATCH 108/776] fix(webui): show message when max rank all warframes has nothing to do --- static/webui/script.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index fd0811d6..f0190b3c 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -675,14 +675,12 @@ function maxRankAllEquipment(categories) { } if (category === "Suits") { if ("exalted" in itemMap[item.ItemType]) { - if (!batchData["SpecialItems"]) { - batchData["SpecialItems"] = []; - } for (const exaltedType of itemMap[item.ItemType].exalted) { const exaltedItem = data["SpecialItems"].find(x => x.ItemType == exaltedType); if (exaltedItem) { const exaltedCap = itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; if (exaltedItem.XP < exaltedCap) { + batchData["SpecialItems"] ??= []; batchData["SpecialItems"].push({ ItemId: { $oid: exaltedItem.ItemId.$oid }, XP: exaltedCap From 137213520edcb4200f9195210a9e1f1d283d001d Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 7 Mar 2025 20:16:15 +0100 Subject: [PATCH 109/776] chore: npm run prettier --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 7e12112f..fde2ff45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ From f7c2c7443762f659c1a318c46bcfe56cf7aa9e20 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 01:44:30 -0800 Subject: [PATCH 110/776] feat: clan xp (#1100) Closes #690 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1100 --- config.json.example | 1 + package-lock.json | 8 +-- package.json | 2 +- .../api/contributeGuildClassController.ts | 66 +++++++++++++++++++ .../contributeToDojoComponentController.ts | 12 +++- .../api/dojoComponentRushController.ts | 4 +- src/controllers/api/getGuildController.ts | 17 ++++- src/controllers/api/guildTechController.ts | 14 +++- .../api/startDojoRecipeController.ts | 5 +- src/models/guildModel.ts | 9 ++- src/routes/api.ts | 2 + src/services/configService.ts | 1 + src/services/guildService.ts | 12 +++- src/types/guildTypes.ts | 18 +++-- static/webui/index.html | 4 ++ static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + 18 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 src/controllers/api/contributeGuildClassController.ts diff --git a/config.json.example b/config.json.example index e710f503..89f272d2 100644 --- a/config.json.example +++ b/config.json.example @@ -34,5 +34,6 @@ "fastDojoRoomDestruction": true, "noDojoResearchCosts": true, "noDojoResearchTime": true, + "fastClanAscension": true, "spoofMasteryRank": -1 } diff --git a/package-lock.json b/package-lock.json index ad2dcbc8..13edcb84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.39", + "warframe-public-export-plus": "^0.5.40", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4083,9 +4083,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.39", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.39.tgz", - "integrity": "sha512-sEGZedtW4I/M2ceoDs6MQ5eHD7sJgv1KRNLt8BWByXLuDa7qTR3Y9px5TGxqt/rBHKGUyPO1LUxu4bDGZi6yXw==" + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.40.tgz", + "integrity": "sha512-/qr46LE/KqDdEkW4z52EG0vZP0Z8U26FscFJ2G5K5ewbQdlSVxtf5fpOnzRkAO7jWWKfgoqx7l5WUgaLSPDj0g==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index aae92ef4..6fcb225d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.39", + "warframe-public-export-plus": "^0.5.40", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts new file mode 100644 index 00000000..d0c91539 --- /dev/null +++ b/src/controllers/api/contributeGuildClassController.ts @@ -0,0 +1,66 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { config } from "@/src/services/configService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const contributeGuildClassController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + const guild = (await Guild.findOne({ _id: payload.GuildId }))!; + + // First contributor initiates ceremony and locks the pending class. + if (!guild.CeremonyContributors) { + guild.CeremonyContributors = []; + guild.CeremonyClass = guildXpToClass(guild.XP); + guild.CeremonyEndo = 0; + for (let i = guild.Class; i != guild.CeremonyClass; ++i) { + guild.CeremonyEndo += (i + 1) * 1000; + } + } + + guild.CeremonyContributors.push(new Types.ObjectId(accountId)); + + // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. + if (guild.CeremonyContributors.length == payload.RequiredContributors) { + guild.Class = guild.CeremonyClass!; + guild.CeremonyClass = undefined; + guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); + } + + await guild.save(); + + // Either way, endo is given to the contributor. + const inventory = await getInventory(accountId, "FusionPoints"); + inventory.FusionPoints += guild.CeremonyEndo!; + await inventory.save(); + + res.json({ + NumContributors: guild.CeremonyContributors.length, + FusionPointReward: guild.CeremonyEndo, + Class: guild.Class, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined + }); +}; + +interface IContributeGuildClassRequest { + GuildId: string; + RequiredContributors: number; +} + +const guildXpToClass = (xp: number): number => { + const cummXp = [ + 0, 11000, 34000, 69000, 114000, 168000, 231000, 302000, 381000, 68000, 563000, 665000, 774000, 891000 + ]; + let highest = 0; + for (let i = 0; i != cummXp.length; ++i) { + if (xp < cummXp[i]) { + break; + } + highest = i; + } + return highest; +}; diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 5aa74855..21be9c82 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -1,13 +1,18 @@ import { TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; -import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; +import { + getDojoClient, + getGuildForRequestEx, + processDojoBuildMaterialsGathered, + scaleRequiredCount +} from "@/src/services/guildService"; import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IDojoContributable } from "@/src/types/guildTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; -import { ExportDojoRecipes, IDojoRecipe } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; interface IContributeToDojoComponentRequest { ComponentId: string; @@ -57,7 +62,7 @@ const processContribution = ( request: IContributeToDojoComponentRequest, inventory: TInventoryDatabaseDocument, inventoryChanges: IInventoryChanges, - meta: IDojoRecipe, + meta: IDojoBuild, component: IDojoContributable ): void => { component.RegularCredits ??= 0; @@ -134,6 +139,7 @@ const processContribution = ( } if (fullyFunded) { component.CompletionTime = new Date(Date.now() + meta.time * 1000); + processDojoBuildMaterialsGathered(guild, meta); } } }; diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index f15bce6c..cb8e0648 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -3,7 +3,7 @@ import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IDojoContributable } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; -import { ExportDojoRecipes, IDojoRecipe } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; interface IDojoComponentRushRequest { DecoType?: string; @@ -40,7 +40,7 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { }); }; -const processContribution = (component: IDojoContributable, meta: IDojoRecipe, platinumDonated: number): void => { +const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => { const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice); const fullDurationSeconds = meta.time; const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 6703d87c..62a27501 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -2,8 +2,9 @@ import { RequestHandler } from "express"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Guild } from "@/src/models/guildModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { getGuildVault } from "@/src/services/guildService"; +import { logger } from "@/src/utils/logger"; const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -15,6 +16,13 @@ const getGuildController: RequestHandler = async (req, res) => { if (inventory.GuildId) { const guild = await Guild.findOne({ _id: inventory.GuildId }); if (guild) { + if (guild.CeremonyResetDate && Date.now() >= guild.CeremonyResetDate.getTime()) { + logger.debug(`ascension ceremony is over`); + guild.CeremonyEndo = undefined; + guild.CeremonyContributors = undefined; + guild.CeremonyResetDate = undefined; + await guild.save(); + } res.json({ _id: toOid(guild._id), Name: guild.Name, @@ -64,7 +72,12 @@ const getGuildController: RequestHandler = async (req, res) => { } ], Tier: 1, - Vault: getGuildVault(guild) + Vault: getGuildVault(guild), + Class: guild.Class, + XP: guild.XP, + IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), + NumContributors: guild.CeremonyContributors?.length ?? 0, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined }); return; } diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index a5a080b1..35dffe2d 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -7,6 +7,7 @@ import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; import { ITechProjectDatabase } from "@/src/types/guildTypes"; +import { TGuildDatabaseDocument } from "@/src/models/guildModel"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -35,7 +36,7 @@ export const guildTechController: RequestHandler = async (req, res) => { }) - 1 ]; if (config.noDojoResearchCosts) { - processFundedProject(techProject, recipe); + processFundedProject(guild, techProject, recipe); } } await guild.save(); @@ -93,7 +94,7 @@ export const guildTechController: RequestHandler = async (req, res) => { if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { // This research is now fully funded. const recipe = ExportDojoRecipes.research[data.RecipeType!]; - processFundedProject(techProject, recipe); + processFundedProject(guild, techProject, recipe); } await guild.save(); @@ -131,9 +132,16 @@ export const guildTechController: RequestHandler = async (req, res) => { } }; -const processFundedProject = (techProject: ITechProjectDatabase, recipe: IDojoResearch): void => { +const processFundedProject = ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase, + recipe: IDojoResearch +): void => { techProject.State = 1; techProject.CompletionDate = new Date(new Date().getTime() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); + if (recipe.guildXpValue) { + guild.XP += recipe.guildXpValue; + } }; type TGuildTechRequest = { diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index 30f4ed75..ee7bb202 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -1,6 +1,6 @@ import { RequestHandler } from "express"; import { IDojoComponentClient } from "@/src/types/guildTypes"; -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequest, processDojoBuildMaterialsGathered } from "@/src/services/guildService"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; import { config } from "@/src/services/configService"; @@ -35,6 +35,9 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { ]; if (config.noDojoRoomBuildStage) { component.CompletionTime = new Date(Date.now()); + if (room) { + processDojoBuildMaterialsGathered(guild, room); + } } await guild.save(); res.json(await getDojoClient(guild, 0)); diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 4fdbc223..7afceb98 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -70,7 +70,14 @@ const guildSchema = new Schema( VaultMiscItems: { type: [typeCountSchema], default: undefined }, VaultShipDecorations: { type: [typeCountSchema], default: undefined }, VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined }, - TechProjects: { type: [techProjectSchema], default: undefined } + TechProjects: { type: [techProjectSchema], default: undefined }, + Class: { type: Number, default: 0 }, + XP: { type: Number, default: 0 }, + ClaimedXP: { type: [String], default: undefined }, + CeremonyClass: Number, + CeremonyContributors: { type: [Types.ObjectId], default: undefined }, + CeremonyResetDate: Date, + CeremonyEndo: Number }, { id: false } ); diff --git a/src/routes/api.ts b/src/routes/api.ts index 2ad923d6..bb63982e 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -13,6 +13,7 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; @@ -153,6 +154,7 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); +apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createGuild.php", createGuildController); diff --git a/src/services/configService.ts b/src/services/configService.ts index c17066c0..84de1f6f 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -60,6 +60,7 @@ interface IConfig { fastDojoRoomDestruction?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; + fastClanAscension?: boolean; spoofMasteryRank?: number; } diff --git a/src/services/guildService.ts b/src/services/guildService.ts index cd877835..9c89f863 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -12,7 +12,7 @@ import { } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; export const getGuildForRequest = async (req: Request): Promise => { @@ -182,3 +182,13 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon guild.VaultPremiumCredits += component.RushPlatinum; } }; + +export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { + if (build.guildXpValue) { + guild.ClaimedXP ??= []; + if (!guild.ClaimedXP.find(x => x == build.resultType)) { + guild.ClaimedXP.push(build.resultType); + guild.XP += build.guildXpValue; + } + } +}; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 4802070f..9effcfbc 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -2,21 +2,29 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "@/src/types/commonTypes"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; -export interface IGuild { - Name: string; -} - -export interface IGuildDatabase extends IGuild { +export interface IGuildDatabase { _id: Types.ObjectId; + Name: string; + DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; DojoEnergy: number; + VaultRegularCredits?: number; VaultPremiumCredits?: number; VaultMiscItems?: IMiscItem[]; VaultShipDecorations?: ITypeCount[]; VaultFusionTreasures?: IFusionTreasure[]; + TechProjects?: ITechProjectDatabase[]; + + Class: number; + XP: number; + ClaimedXP?: string[]; // track rooms and decos that have already granted XP + CeremonyClass?: number; + CeremonyEndo?: number; + CeremonyContributors?: Types.ObjectId[]; + CeremonyResetDate?: Date; } export interface IGuildVault { diff --git a/static/webui/index.html b/static/webui/index.html index a161bd0c..bb1c88df 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -537,6 +537,10 @@
+
+ + +
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index bc1472c0..484b0063 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -114,6 +114,7 @@ dict = { cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchTime: `No Dojo Research Time`, + cheats_fastClanAscension: `Fast Clan Ascension`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, cheats_saveSettings: `Save Settings`, cheats_account: `Account`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 5acce802..153aad2b 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -115,6 +115,7 @@ dict = { cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `Aucun coût de recherche (Dojo)`, cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, + cheats_fastClanAscension: `[UNTRANSLATED] Fast Clan Ascension`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, cheats_saveSettings: `Sauvegarder les paramètres`, cheats_account: `Compte`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 3da49d4d..44c6eb41 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -115,6 +115,7 @@ dict = { cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, + cheats_fastClanAscension: `[UNTRANSLATED] Fast Clan Ascension`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, cheats_saveSettings: `Сохранить настройки`, cheats_account: `Аккаунт`, From 9acad90b12bc61feca05bc7582576ff7aef9d549 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 01:44:54 -0800 Subject: [PATCH 111/776] chore: add Invasions to worldState (#1102) Re #1097 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1102 --- .../worldState/worldState.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 9e10a38c..2105a9c5 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -21,6 +21,46 @@ "Icon": "/Lotus/Interface/Icons/DiscordIconNoBacker.png" } ], + "Invasions": [ + { + "_id": { + "$oid": "67c8ec8b3d0d86b236c1c18f" + }, + "Faction": "FC_INFESTATION", + "DefenderFaction": "FC_CORPUS", + "Node": "SolNode53", + "Count": -28558, + "Goal": 30000, + "LocTag": "/Lotus/Language/Menu/InfestedInvasionBoss", + "Completed": false, + "ChainID": { + "$oid": "67c8b6a2bde0dfd0f7c1c18d" + }, + "AttackerReward": [], + "AttackerMissionInfo": { + "seed": 488863, + "faction": "FC_CORPUS" + }, + "DefenderReward": { + "countedItems": [ + { + "ItemType": "/Lotus/Types/Items/Research/EnergyComponent", + "ItemCount": 3 + } + ] + }, + "DefenderMissionInfo": { + "seed": 127653, + "faction": "FC_INFESTATION", + "missionReward": [] + }, + "Activation": { + "$date": { + "$numberLong": "1741221003031" + } + } + } + ], "Sorties": [ { "_id": { "$oid": "663a4c7d4d932c97c0a3acd7" }, From 6c7e8e908e09f8358231cb90b0ef7f8af53b56c5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 03:36:52 -0800 Subject: [PATCH 112/776] fix: set ArchwingEnabled to true when obtaining an archwing (#1091) Closes #984 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1091 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 19fda5e9..9139d41c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -451,6 +451,7 @@ export const addItem = async ( }; } case "Archwing": { + inventory.ArchwingEnabled = true; return { InventoryChanges: { ...addSpaceSuit( From 5a843dfe534dceeef28befc57d65a427663a0b3c Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:26:24 -0800 Subject: [PATCH 113/776] fix: icon for welcome message (#1115) Closes #1114 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1115 Co-authored-by: Sainan Co-committed-by: Sainan --- static/fixed_responses/eventMessages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/fixed_responses/eventMessages.json b/static/fixed_responses/eventMessages.json index 6ecf6d44..62cb477a 100644 --- a/static/fixed_responses/eventMessages.json +++ b/static/fixed_responses/eventMessages.json @@ -4,7 +4,7 @@ "sub": "Welcome to Space Ninja Server", "sndr": "/Lotus/Language/Bosses/Ordis", "msg": "Enjoy your Space Ninja Experience", - "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", + "icon": "/Lotus/Interface/Icons/Npcs/Ordis.png", "eventMessageDate": "2025-01-30T13:00:00.000Z", "r": false } From 537fe5dcd10ff7e6aee76661586788969f632463 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:27:11 -0800 Subject: [PATCH 114/776] fix: ensure exalted weapons are given from giveStartingGear (#1092) Fixes #1020 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1092 Co-authored-by: Sainan Co-committed-by: Sainan --- src/controllers/api/giveStartingGearController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index 118664b3..b8b09b2e 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -3,6 +3,7 @@ import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryMo import { addEquipment, addItem, + addPowerSuit, combineInventoryChanges, getInventory, updateSlots @@ -52,7 +53,7 @@ export const addStartingGear = async ( addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Suits", Suits[0].ItemType, undefined, inventoryChanges, { Configs: Suits[0].Configs }); + addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); addEquipment( inventory, "DataKnives", From 6142b8d2dc5a6572c054db48d728c734b933c9a1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:28:05 -0800 Subject: [PATCH 115/776] feat: config option for star days event (#1104) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1104 Co-authored-by: Sainan Co-committed-by: Sainan --- config.json.example | 5 ++- .../dynamic/worldStateController.ts | 36 +++++++++++++++++++ src/services/configService.ts | 3 ++ .../worldState/worldState.json | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 89f272d2..d85b0072 100644 --- a/config.json.example +++ b/config.json.example @@ -35,5 +35,8 @@ "noDojoResearchCosts": true, "noDojoResearchTime": true, "fastClanAscension": true, - "spoofMasteryRank": -1 + "spoofMasteryRank": -1, + "events": { + "starDays": true + } } diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index a954aae9..3341992e 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -7,6 +7,7 @@ import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winte import { buildConfig } from "@/src/services/buildConfigService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { config } from "@/src/services/configService"; export const worldStateController: RequestHandler = (req, res) => { const worldState: IWorldState = { @@ -15,10 +16,28 @@ export const worldStateController: RequestHandler = (req, res) => { ? req.query.buildLabel.split(" ").join("+") : buildConfig.buildLabel, Time: Math.round(Date.now() / 1000), + Goals: [], EndlessXpChoices: [], ...staticWorldState }; + if (config.events?.starDays) { + worldState.Goals.push({ + _id: { $oid: "67a4dcce2a198564d62e1647" }, + Activation: { $date: { $numberLong: "1738868400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + Desc: "/Lotus/Language/Events/ValentinesFortunaName", + ToolTip: "/Lotus/Language/Events/ValentinesFortunaName", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png", + Tag: "FortunaValentines", + Node: "SolarisUnitedHub1" + }); + } + const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 const day = Math.trunc((new Date().getTime() - EPOCH) / 86400000); const week = Math.trunc(day / 7); @@ -134,8 +153,10 @@ export const worldStateController: RequestHandler = (req, res) => { }; interface IWorldState { + Version: number; // for goals BuildLabel: string; Time: number; + Goals: IGoal[]; SyndicateMissions: ISyndicateMission[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; @@ -143,6 +164,21 @@ interface IWorldState { Tmp?: string; } +interface IGoal { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Count: number; + Goal: number; + Success: number; + Personal: boolean; + Desc: string; + ToolTip: string; + Icon: string; + Tag: string; + Node: string; +} + interface ISyndicateMission { _id: IOid; Activation: IMongoDate; diff --git a/src/services/configService.ts b/src/services/configService.ts index 84de1f6f..07860f93 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -62,6 +62,9 @@ interface IConfig { noDojoResearchTime?: boolean; fastClanAscension?: boolean; spoofMasteryRank?: number; + events?: { + starDays?: boolean; + }; } interface ILoggerConfig { diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 2105a9c5..047dfd86 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -1,4 +1,5 @@ { + "Version": 10, "Events": [ { "Msg": "Join the OpenWF Discord!", From ec1f504bae3d44a1dd4d853d17bd672687f18c4c Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:33:33 -0800 Subject: [PATCH 116/776] chore(webui): allow negative quantity for "add items" & "add mods" (#1113) Closes #1111 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1113 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/inventoryService.ts | 3 +++ static/webui/index.html | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9139d41c..ccfaf0d0 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -284,6 +284,9 @@ export const addItem = async ( }; } else if (ExportResources[typeName].productCategory == "KubrowPetEggs") { const changes: IKubrowPetEggClient[] = []; + if (quantity < 0) { + throw new Error(`removal of KubrowPetEggs not handled`); + } for (let i = 0; i != quantity; ++i) { const egg: IKubrowPetEggDatabase = { ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", diff --git a/static/webui/index.html b/static/webui/index.html index bb1c88df..f700994d 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -89,7 +89,7 @@
- +
@@ -392,7 +392,7 @@
- +
From 457663f14a037f89f060e89ccfd57fc8818c0466 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:33:45 -0800 Subject: [PATCH 117/776] fix: claim recipe response (#1106) Fixes #1105 The client already 'knows' the ItemCount was decremented so when we also say it in the response, it actually ends up causing the client to think the recipe was used twice. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1106 Co-authored-by: Sainan Co-committed-by: Sainan --- src/controllers/api/claimCompletedRecipeController.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 47d606bf..325d10ff 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -95,16 +95,12 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = let InventoryChanges = {}; if (recipe.consumeOnUse) { - const recipeChanges = [ + addRecipes(inventory, [ { ItemType: pendingRecipe.ItemType, ItemCount: -1 } - ]; - - InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges }; - - addRecipes(inventory, recipeChanges); + ]); } if (req.query.rush) { InventoryChanges = { From 7fdb37f2e828d704ead5c7ae97005ce83dbb7a5d Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:34:00 -0800 Subject: [PATCH 118/776] fix: donate platinum from clan vault (#1107) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1107 Co-authored-by: Sainan Co-committed-by: Sainan --- src/controllers/api/dojoComponentRushController.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index cb8e0648..e72531f4 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -21,17 +21,22 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest; const component = guild.DojoComponents.id(request.ComponentId)!; + let platinumDonated = request.Amount; + const inventoryChanges = updateCurrency(inventory, request.Amount, true); + if (request.VaultAmount) { + platinumDonated += request.VaultAmount; + guild.VaultPremiumCredits! -= request.VaultAmount; + } + if (request.DecoId) { const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; - processContribution(deco, meta, request.Amount); + processContribution(deco, meta, platinumDonated); } else { const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; - processContribution(component, meta, request.Amount); + processContribution(component, meta, platinumDonated); } - const inventoryChanges = updateCurrency(inventory, request.Amount, true); - await guild.save(); await inventory.save(); res.json({ From d7e3f33ecfec08548b2493e2fd355588e9d50e95 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:34:14 -0800 Subject: [PATCH 119/776] feat: add custom getName endpoint (#1108) This can be useful for an IRC server to validate the accountId & nonce given and ensure the nickname matches. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1108 Co-authored-by: Sainan Co-committed-by: Sainan --- src/controllers/custom/getNameController.ts | 7 +++++++ src/routes/custom.ts | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 src/controllers/custom/getNameController.ts diff --git a/src/controllers/custom/getNameController.ts b/src/controllers/custom/getNameController.ts new file mode 100644 index 00000000..bc4a94f3 --- /dev/null +++ b/src/controllers/custom/getNameController.ts @@ -0,0 +1,7 @@ +import { RequestHandler } from "express"; +import { getAccountForRequest } from "@/src/services/loginService"; + +export const getNameController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + res.json(account.DisplayName); +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index a69afc1c..00fe18f3 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -5,6 +5,7 @@ import { getItemListsController } from "@/src/controllers/custom/getItemListsCon import { pushArchonCrystalUpgradeController } from "@/src/controllers/custom/pushArchonCrystalUpgradeController"; import { popArchonCrystalUpgradeController } from "@/src/controllers/custom/popArchonCrystalUpgradeController"; import { deleteAccountController } from "@/src/controllers/custom/deleteAccountController"; +import { getNameController } from "@/src/controllers/custom/getNameController"; import { renameAccountController } from "@/src/controllers/custom/renameAccountController"; import { createAccountController } from "@/src/controllers/custom/createAccountController"; @@ -25,6 +26,7 @@ customRouter.get("/getItemLists", getItemListsController); customRouter.get("/pushArchonCrystalUpgrade", pushArchonCrystalUpgradeController); customRouter.get("/popArchonCrystalUpgrade", popArchonCrystalUpgradeController); customRouter.get("/deleteAccount", deleteAccountController); +customRouter.get("/getName", getNameController); customRouter.get("/renameAccount", renameAccountController); customRouter.post("/createAccount", createAccountController); From 901263ada3a96874dc10394f8b2a735c625d80b5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 04:34:41 -0800 Subject: [PATCH 120/776] feat: transmutation (#1112) Closes #1098 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1112 Co-authored-by: Sainan Co-committed-by: Sainan --- package-lock.json | 8 +- package.json | 2 +- .../api/activateRandomModController.ts | 69 +--------- .../api/artifactTransmutationController.ts | 124 ++++++++++++++++++ .../completeRandomModChallengeController.ts | 2 +- .../api/rerollRandomModController.ts | 6 +- src/helpers/rivenFingerprintHelper.ts | 65 --------- src/helpers/rivenHelper.ts | 121 +++++++++++++++++ src/routes/api.ts | 2 + 9 files changed, 260 insertions(+), 139 deletions(-) create mode 100644 src/controllers/api/artifactTransmutationController.ts delete mode 100644 src/helpers/rivenFingerprintHelper.ts create mode 100644 src/helpers/rivenHelper.ts diff --git a/package-lock.json b/package-lock.json index 13edcb84..99bdfa62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.40", + "warframe-public-export-plus": "^0.5.41", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4083,9 +4083,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.40", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.40.tgz", - "integrity": "sha512-/qr46LE/KqDdEkW4z52EG0vZP0Z8U26FscFJ2G5K5ewbQdlSVxtf5fpOnzRkAO7jWWKfgoqx7l5WUgaLSPDj0g==" + "version": "0.5.41", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.41.tgz", + "integrity": "sha512-qVOUY4UjF1cyBrBbMwD25xHSdSf9q57/CJgjHsfSE7NUu/6pBDSZzwS0iAetAukws/1V2kDvsuy8AGtOec2L1w==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 6fcb225d..50710c25 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.40", + "warframe-public-export-plus": "^0.5.41", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/activateRandomModController.ts b/src/controllers/api/activateRandomModController.ts index bdf67212..4ac822c7 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -1,10 +1,9 @@ import { toOid } from "@/src/helpers/inventoryHelpers"; -import { IRivenChallenge } from "@/src/helpers/rivenFingerprintHelper"; +import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { addMods, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getRandomElement, getRandomInt, getRandomReward } from "@/src/services/rngService"; -import { logger } from "@/src/utils/logger"; +import { getRandomElement } from "@/src/services/rngService"; import { RequestHandler } from "express"; import { ExportUpgrades } from "warframe-public-export-plus"; @@ -19,40 +18,18 @@ export const activateRandomModController: RequestHandler = async (req, res) => { } ]); const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]); - const challenge = getRandomElement(ExportUpgrades[rivenType].availableChallenges!); - const fingerprintChallenge: IRivenChallenge = { - Type: challenge.fullName, - Progress: 0, - Required: getRandomInt(challenge.countRange[0], challenge.countRange[1]) - }; - if (Math.random() < challenge.complicationChance) { - const complications: { type: string; probability: number }[] = []; - for (const complication of challenge.complications) { - complications.push({ - type: complication.fullName, - probability: complication.weight - }); - } - fingerprintChallenge.Complication = getRandomReward(complications)!.type; - logger.debug( - `riven rolled challenge ${fingerprintChallenge.Type} with complication ${fingerprintChallenge.Complication}` - ); - const complication = challenge.complications.find(x => x.fullName == fingerprintChallenge.Complication)!; - fingerprintChallenge.Required *= complication.countMultiplier; - } else { - logger.debug(`riven rolled challenge ${fingerprintChallenge.Type}`); - } + const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const upgradeIndex = inventory.Upgrades.push({ ItemType: rivenType, - UpgradeFingerprint: JSON.stringify({ challenge: fingerprintChallenge }) + UpgradeFingerprint: JSON.stringify(fingerprint) }) - 1; await inventory.save(); // For some reason, in this response, the UpgradeFingerprint is simply a nested object and not a string res.json({ NewMod: { - UpgradeFingerprint: { challenge: fingerprintChallenge }, - ItemType: inventory.Upgrades[upgradeIndex].ItemType, + UpgradeFingerprint: fingerprint, + ItemType: rivenType, ItemId: toOid(inventory.Upgrades[upgradeIndex]._id) } }); @@ -61,37 +38,3 @@ export const activateRandomModController: RequestHandler = async (req, res) => { interface IActiveRandomModRequest { ItemType: string; } - -const rivenRawToRealWeighted: Record = { - "/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawMeleeRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawModularMeleeRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusModularMeleeRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawModularPistolRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusModularPistolRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawPistolRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare"], - "/Lotus/Upgrades/Mods/Randomized/RawRifleRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare"], - "/Lotus/Upgrades/Mods/Randomized/RawShotgunRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawSentinelWeaponRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" - ] -}; diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts new file mode 100644 index 00000000..3fd52535 --- /dev/null +++ b/src/controllers/api/artifactTransmutationController.ts @@ -0,0 +1,124 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; +import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; +import { IOid } from "@/src/types/commonTypes"; +import { RequestHandler } from "express"; +import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus"; + +export const artifactTransmutationController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = JSON.parse(String(req.body)) as IArtifactTransmutationRequest; + + inventory.RegularCredits -= payload.Cost; + inventory.FusionPoints -= payload.FusionPointCost; + + if (payload.RivenTransmute) { + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientSecretItem", + ItemCount: -1 + } + ]); + + payload.Consumed.forEach(upgrade => { + inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid }); + }); + + const rawRivenType = getRandomRawRivenType(); + const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]); + const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); + + const upgradeIndex = + inventory.Upgrades.push({ + ItemType: rivenType, + UpgradeFingerprint: JSON.stringify(fingerprint) + }) - 1; + await inventory.save(); + res.json({ + NewMods: [ + { + ItemId: toOid(inventory.Upgrades[upgradeIndex]._id), + ItemType: rivenType, + UpgradeFingerprint: fingerprint + } + ] + }); + } else { + const counts: Record = { + COMMON: 0, + UNCOMMON: 0, + RARE: 0, + LEGENDARY: 0 + }; + payload.Consumed.forEach(upgrade => { + const meta = ExportUpgrades[upgrade.ItemType]; + counts[meta.rarity] += upgrade.ItemCount; + addMods(inventory, [ + { + ItemType: upgrade.ItemType, + ItemCount: upgrade.ItemCount * -1 + } + ]); + }); + + // Based on the table on https://wiki.warframe.com/w/Transmutation + const weights: Record = { + COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4, + UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10, + RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50, + LEGENDARY: 0 + }; + + const options: { uniqueName: string; rarity: TRarity }[] = []; + Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => { + if (upgrade.canBeTransmutation) { + options.push({ uniqueName, rarity: upgrade.rarity }); + } + }); + + const newModType = getRandomWeightedReward(options, weights)!.uniqueName; + addMods(inventory, [ + { + ItemType: newModType, + ItemCount: 1 + } + ]); + + await inventory.save(); + res.json({ + NewMods: [ + { + ItemType: newModType, + ItemCount: 1 + } + ] + }); + } +}; + +const getRandomRawRivenType = (): string => { + const pack = ExportBoosterPacks["/Lotus/Types/BoosterPacks/CalendarRivenPack"]; + return getRandomWeightedRewardUc(pack.components, pack.rarityWeightsPerRoll[0])!.Item; +}; + +interface IArtifactTransmutationRequest { + Upgrade: IAgnosticUpgradeClient; + LevelDiff: number; + Consumed: IAgnosticUpgradeClient[]; + Cost: number; + FusionPointCost: number; + RivenTransmute?: boolean; +} + +interface IAgnosticUpgradeClient { + ItemType: string; + ItemId: IOid; + FromSKU: boolean; + UpgradeFingerprint: string; + PendingRerollFingerprint: string; + ItemCount: number; + LastAdded: IOid; +} diff --git a/src/controllers/api/completeRandomModChallengeController.ts b/src/controllers/api/completeRandomModChallengeController.ts index ef5e7d2a..a4e3cf08 100644 --- a/src/controllers/api/completeRandomModChallengeController.ts +++ b/src/controllers/api/completeRandomModChallengeController.ts @@ -4,7 +4,7 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenFingerprintHelper"; +import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; export const completeRandomModChallengeController: RequestHandler = async (req, res) => { diff --git a/src/controllers/api/rerollRandomModController.ts b/src/controllers/api/rerollRandomModController.ts index 9dc84e5f..20e7218a 100644 --- a/src/controllers/api/rerollRandomModController.ts +++ b/src/controllers/api/rerollRandomModController.ts @@ -2,11 +2,7 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { - createUnveiledRivenFingerprint, - randomiseRivenStats, - RivenFingerprint -} from "@/src/helpers/rivenFingerprintHelper"; +import { createUnveiledRivenFingerprint, randomiseRivenStats, RivenFingerprint } from "@/src/helpers/rivenHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; import { IOid } from "@/src/types/commonTypes"; diff --git a/src/helpers/rivenFingerprintHelper.ts b/src/helpers/rivenFingerprintHelper.ts deleted file mode 100644 index e5fa261d..00000000 --- a/src/helpers/rivenFingerprintHelper.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { IUpgrade } from "warframe-public-export-plus"; -import { getRandomElement, getRandomInt } from "../services/rngService"; - -export type RivenFingerprint = IVeiledRivenFingerprint | IUnveiledRivenFingerprint; - -export interface IVeiledRivenFingerprint { - challenge: IRivenChallenge; -} - -export interface IRivenChallenge { - Type: string; - Progress: number; - Required: number; - Complication?: string; -} - -export interface IUnveiledRivenFingerprint { - compat: string; - lim: 0; - lvl: number; - lvlReq: number; - rerolls?: number; - pol: string; - buffs: IRivenStat[]; - curses: IRivenStat[]; -} - -interface IRivenStat { - Tag: string; - Value: number; -} - -export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => { - const fingerprint: IUnveiledRivenFingerprint = { - compat: getRandomElement(meta.compatibleItems!), - lim: 0, - lvl: 0, - lvlReq: getRandomInt(8, 16), - pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), - buffs: [], - curses: [] - }; - randomiseRivenStats(meta, fingerprint); - return fingerprint; -}; - -export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenFingerprint): void => { - fingerprint.buffs = []; - const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 - const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff); - for (let i = 0; i != numBuffs; ++i) { - const buffIndex = Math.trunc(Math.random() * buffEntries.length); - const entry = buffEntries[buffIndex]; - fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - buffEntries.splice(buffIndex, 1); - } - - fingerprint.curses = []; - if (Math.random() < 0.5) { - const entry = getRandomElement( - meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) - ); - fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - } -}; diff --git a/src/helpers/rivenHelper.ts b/src/helpers/rivenHelper.ts new file mode 100644 index 00000000..e3819b64 --- /dev/null +++ b/src/helpers/rivenHelper.ts @@ -0,0 +1,121 @@ +import { IUpgrade } from "warframe-public-export-plus"; +import { getRandomElement, getRandomInt, getRandomReward } from "../services/rngService"; + +export type RivenFingerprint = IVeiledRivenFingerprint | IUnveiledRivenFingerprint; + +export interface IVeiledRivenFingerprint { + challenge: IRivenChallenge; +} + +export interface IRivenChallenge { + Type: string; + Progress: number; + Required: number; + Complication?: string; +} + +export interface IUnveiledRivenFingerprint { + compat: string; + lim: 0; + lvl: number; + lvlReq: number; + rerolls?: number; + pol: string; + buffs: IRivenStat[]; + curses: IRivenStat[]; +} + +interface IRivenStat { + Tag: string; + Value: number; +} + +export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFingerprint => { + const challenge = getRandomElement(meta.availableChallenges!); + const fingerprintChallenge: IRivenChallenge = { + Type: challenge.fullName, + Progress: 0, + Required: getRandomInt(challenge.countRange[0], challenge.countRange[1]) + }; + if (Math.random() < challenge.complicationChance) { + const complications: { type: string; probability: number }[] = []; + for (const complication of challenge.complications) { + complications.push({ + type: complication.fullName, + probability: complication.weight + }); + } + fingerprintChallenge.Complication = getRandomReward(complications)!.type; + const complication = challenge.complications.find(x => x.fullName == fingerprintChallenge.Complication)!; + fingerprintChallenge.Required *= complication.countMultiplier; + } + return { challenge: fingerprintChallenge }; +}; + +export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => { + const fingerprint: IUnveiledRivenFingerprint = { + compat: getRandomElement(meta.compatibleItems!), + lim: 0, + lvl: 0, + lvlReq: getRandomInt(8, 16), + pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), + buffs: [], + curses: [] + }; + randomiseRivenStats(meta, fingerprint); + return fingerprint; +}; + +export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenFingerprint): void => { + fingerprint.buffs = []; + const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 + const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff); + for (let i = 0; i != numBuffs; ++i) { + const buffIndex = Math.trunc(Math.random() * buffEntries.length); + const entry = buffEntries[buffIndex]; + fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + buffEntries.splice(buffIndex, 1); + } + + fingerprint.curses = []; + if (Math.random() < 0.5) { + const entry = getRandomElement( + meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) + ); + fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } +}; + +export const rivenRawToRealWeighted: Record = { + "/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawMeleeRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawModularMeleeRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusModularMeleeRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawModularPistolRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusModularPistolRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawPistolRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare"], + "/Lotus/Upgrades/Mods/Randomized/RawRifleRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare"], + "/Lotus/Upgrades/Mods/Randomized/RawShotgunRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawSentinelWeaponRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" + ] +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index bb63982e..964ebd5d 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -7,6 +7,7 @@ import { addFriendImageController } from "@/src/controllers/api/addFriendImageCo import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; +import { artifactTransmutationController } from "@/src/controllers/api/artifactTransmutationController"; import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; @@ -150,6 +151,7 @@ apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/artifacts.php", artifactsController); +apiRouter.post("/artifactTransmutation.php", artifactTransmutationController); apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); From 3853fda60d0f8fe657922820a0587be84b6ff5f5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 05:36:06 -0800 Subject: [PATCH 121/776] feat: track NemesisAbandonedRewards (#1118) Re #446 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1118 Co-authored-by: Sainan Co-committed-by: Sainan --- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/missionInventoryUpdateService.ts | 3 +++ src/types/requestTypes.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 49634ca0..280934b8 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1290,7 +1290,7 @@ const inventorySchema = new Schema( InvasionChainProgress: [Schema.Types.Mixed], //CorpusLich or GrineerLich - NemesisAbandonedRewards: [String], + NemesisAbandonedRewards: { type: [String], default: [] }, //CorpusLich\KuvaLich NemesisHistory: [Schema.Types.Mixed], LastNemesisAllySpawnTime: Schema.Types.Mixed, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 45cb1e6a..6c27d51e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -84,6 +84,9 @@ export const addMissionInventoryUpdates = async ( }); } } + if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { + inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; + } for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { if (value === undefined) { logger.error(`Inventory update key ${key} has no value `); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 96ac7619..441e8d63 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -117,6 +117,7 @@ export interface IRewardInfo { toxinOk?: boolean; lostTargetWave?: number; defenseTargetCount?: number; + NemesisAbandonedRewards?: string[]; EOM_AFK?: number; rewardQualifications?: string; // did a Survival for 5 minutes and this was "1" PurgatoryRewardQualifications?: string; From 92d53e1c00570fc70e15a243f8f70c46f93ae748 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 8 Mar 2025 06:29:05 -0800 Subject: [PATCH 122/776] chore: improve addMissionInventoryUpdates (#1121) Closes #1119 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1121 Co-authored-by: Sainan Co-committed-by: Sainan --- src/services/missionInventoryUpdateService.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 6c27d51e..c9986c38 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -65,12 +65,8 @@ const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { export const addMissionInventoryUpdates = async ( inventory: HydratedDocument, inventoryUpdates: IMissionInventoryUpdateRequest -): Promise | undefined> => { - //TODO: type this properly - const inventoryChanges: Partial = {}; - if (inventoryUpdates.MissionFailed === true) { - return; - } +): Promise => { + const inventoryChanges: IInventoryChanges = {}; if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.periodicMissionTag) { const tag = inventoryUpdates.RewardInfo.periodicMissionTag; const existingCompletion = inventory.PeriodicMissionCompletions.find(completion => completion.tag === tag); From 3af15881f5fc3db6b28981a6509732f888c0479e Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 03:41:12 -0700 Subject: [PATCH 123/776] fix: failure to remove shard installed via webui (#1129) Fixes #1128 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1129 --- .../api/infestedFoundryController.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index acce1508..1a0ac5f7 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -57,17 +57,18 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; - // refund shard - const shard = Object.entries(colorToShard).find( - ([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color - )![1]; - const miscItemChanges = [ - { + const miscItemChanges: IMiscItem[] = []; + if (suit.ArchonCrystalUpgrades![request.Slot].Color) { + // refund shard + const shard = Object.entries(colorToShard).find( + ([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color + )![1]; + miscItemChanges.push({ ItemType: shard, ItemCount: 1 - } - ]; - addMiscItems(inventory, miscItemChanges); + }); + addMiscItems(inventory, miscItemChanges); + } // remove from suit suit.ArchonCrystalUpgrades![request.Slot] = {}; From 3da02385f9e975f8cc9803f40b8bc9509875ec13 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 05:45:11 -0700 Subject: [PATCH 124/776] chore: auto-detect 'my address', only use config as fallback (#1125) This is useful for LAN usage where we can use localhost on our own machine but have to use 192.168.x.y on other devices. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1125 --- src/controllers/api/loginController.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 5293641f..e1fbbb43 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -19,6 +19,8 @@ export const loginController: RequestHandler = async (request, response) => { ? request.query.buildLabel.split(" ").join("+") : buildConfig.buildLabel; + const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress; + if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { try { const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); @@ -44,7 +46,7 @@ export const loginController: RequestHandler = async (request, response) => { LatestEventMessageDate: new Date(0) }); logger.debug("created new account"); - response.json(createLoginResponse(newAccount, buildLabel)); + response.json(createLoginResponse(myAddress, newAccount, buildLabel)); return; } catch (error: unknown) { if (error instanceof Error) { @@ -67,10 +69,10 @@ export const loginController: RequestHandler = async (request, response) => { } await account.save(); - response.json(createLoginResponse(account.toJSON(), buildLabel)); + response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel)); }; -const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { +const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { return { id: account.id, DisplayName: account.DisplayName, @@ -84,9 +86,9 @@ const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): TrackedSettings: account.TrackedSettings, Nonce: account.Nonce, Groups: [], - IRC: config.myIrcAddresses ?? [config.myAddress], - platformCDNs: [`https://${config.myAddress}/`], - HUB: `https://${config.myAddress}/api/`, + IRC: config.myIrcAddresses ?? [myAddress], + platformCDNs: [`https://${myAddress}/`], + HUB: `https://${myAddress}/api/`, NRS: config.NRS, DTLS: 99, BuildLabel: buildLabel, From f6513420be12f1c318af38dd639af590c581774c Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 07:40:37 -0700 Subject: [PATCH 125/776] feat: login conflict (#1127) Closes #1076 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1127 --- src/controllers/api/loginController.ts | 15 ++++++++++++--- .../custom/ircDroppedController.ts | 9 +++++++++ src/models/loginModel.ts | 1 + src/routes/custom.ts | 2 ++ src/services/loginService.ts | 19 +++++-------------- src/types/loginTypes.ts | 2 ++ 6 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 src/controllers/custom/ircDroppedController.ts diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index e1fbbb43..0e7c53fb 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -61,10 +61,19 @@ export const loginController: RequestHandler = async (request, response) => { return; } - if (account.Nonce == 0 || loginRequest.ClientType != "webui") { + if (loginRequest.ClientType == "webui") { + if (!account.Nonce) { + account.ClientType = "webui"; + account.Nonce = nonce; + } + } else { + if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) { + response.status(400).json({ error: "nonce still set" }); + return; + } + + account.ClientType = loginRequest.ClientType; account.Nonce = nonce; - } - if (loginRequest.ClientType != "webui") { account.CountryCode = loginRequest.lang.toUpperCase(); } await account.save(); diff --git a/src/controllers/custom/ircDroppedController.ts b/src/controllers/custom/ircDroppedController.ts new file mode 100644 index 00000000..8927c5bb --- /dev/null +++ b/src/controllers/custom/ircDroppedController.ts @@ -0,0 +1,9 @@ +import { getAccountForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const ircDroppedController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + account.Dropped = true; + await account.save(); + res.end(); +}; diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index eb3d1576..75a12356 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -20,6 +20,7 @@ const databaseAccountSchema = new Schema( ConsentNeeded: { type: Boolean, required: true }, TrackedSettings: { type: [String], default: [] }, Nonce: { type: Number, default: 0 }, + Dropped: Boolean, LastLoginDay: { type: Number }, LatestEventMessageDate: { type: Date, default: 0 } }, diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 00fe18f3..7f53ad3e 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -7,6 +7,7 @@ import { popArchonCrystalUpgradeController } from "@/src/controllers/custom/popA import { deleteAccountController } from "@/src/controllers/custom/deleteAccountController"; import { getNameController } from "@/src/controllers/custom/getNameController"; import { renameAccountController } from "@/src/controllers/custom/renameAccountController"; +import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController"; import { createAccountController } from "@/src/controllers/custom/createAccountController"; import { createMessageController } from "@/src/controllers/custom/createMessageController"; @@ -28,6 +29,7 @@ customRouter.get("/popArchonCrystalUpgrade", popArchonCrystalUpgradeController); customRouter.get("/deleteAccount", deleteAccountController); customRouter.get("/getName", getNameController); customRouter.get("/renameAccount", renameAccountController); +customRouter.get("/ircDropped", ircDroppedController); customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 35b3feea..71236f07 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -82,21 +82,12 @@ export const getAccountForRequest = async (req: Request): Promise => { - if (!req.query.accountId) { - throw new Error("Request is missing accountId parameter"); + const account = await getAccountForRequest(req); + if (account.Dropped && req.query.ct) { + account.Dropped = undefined; + await account.save(); } - if (!req.query.nonce || parseInt(req.query.nonce as string) === 0) { - throw new Error("Request is missing nonce parameter"); - } - if ( - !(await Account.exists({ - _id: req.query.accountId, - Nonce: req.query.nonce - })) - ) { - throw new Error("Invalid accountId-nonce pair"); - } - return req.query.accountId as string; + return account._id.toString(); }; export const isAdministrator = (account: TAccountDocument): boolean => { diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 687d611e..108b0417 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -14,6 +14,7 @@ export interface IAccountAndLoginResponseCommons { export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { email: string; password: string; + Dropped?: boolean; LastLoginDay?: number; LatestEventMessageDate: Date; } @@ -32,6 +33,7 @@ export interface ILoginRequest { date: number; ClientType: string; PS: string; + kick?: boolean; } export interface ILoginResponse extends IAccountAndLoginResponseCommons { From 6b35408144d1ac905853c5eb47e57e9943969811 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 07:41:24 -0700 Subject: [PATCH 126/776] chore: don't install dev dependencies for basic usage (#1135) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1135 --- UPDATE AND START SERVER.bat | 2 +- package.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index 7aab86b3..8fe5b00c 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -13,7 +13,7 @@ if exist static\data\0\ ( ) echo Updating dependencies... -call npm i +call npm i --omit=dev call npm run build if %errorlevel% == 0 ( diff --git a/package.json b/package.json index 50710c25..a57639f6 100644 --- a/package.json +++ b/package.json @@ -14,27 +14,27 @@ }, "license": "GNU", "dependencies": { + "@types/express": "^5", + "@types/morgan": "^1.9.9", "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "typescript": ">=4.7.4 <5.6.0", "warframe-public-export-plus": "^0.5.41", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@types/express": "^5", - "@types/morgan": "^1.9.9", "@typescript-eslint/eslint-plugin": "^7.18", "@typescript-eslint/parser": "^7.18", "eslint": "^8.56.0", "eslint-plugin-prettier": "^5.2.3", - "morgan": "^1.10.0", "prettier": "^3.4.2", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "tsconfig-paths": "^4.2.0", - "typescript": ">=4.7.4 <5.6.0" + "tsconfig-paths": "^4.2.0" }, "engines": { "node": ">=18.15.0", From 1c276ce133de71a3440d00655bbd7d162170385b Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 07:42:55 -0700 Subject: [PATCH 127/776] feat: stripped rewards (#1123) Closes #683 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1123 --- package-lock.json | 8 ++--- package.json | 2 +- src/controllers/api/dronesController.ts | 3 +- src/services/inventoryService.ts | 4 +-- src/services/itemDataService.ts | 8 +++++ src/services/missionInventoryUpdateService.ts | 30 +++++++++++++++++-- src/services/purchaseService.ts | 6 ++-- src/services/questService.ts | 8 ++--- src/types/requestTypes.ts | 4 +++ 9 files changed, 56 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99bdfa62..98c06df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5", "mongoose": "^8.11.0", - "warframe-public-export-plus": "^0.5.41", + "warframe-public-export-plus": "^0.5.42", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4083,9 +4083,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.41", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.41.tgz", - "integrity": "sha512-qVOUY4UjF1cyBrBbMwD25xHSdSf9q57/CJgjHsfSE7NUu/6pBDSZzwS0iAetAukws/1V2kDvsuy8AGtOec2L1w==" + "version": "0.5.42", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.42.tgz", + "integrity": "sha512-up3P5bLKD42Xkr3o7TX9WUwvpJzK88aQTLZ2bB6QWUHdsJxl/Z3TBn+HSd3eouIDTMVUzbTDeDPosSw7TcLegA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index a57639f6..1a931ddf 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=4.7.4 <5.6.0", - "warframe-public-export-plus": "^0.5.41", + "warframe-public-export-plus": "^0.5.42", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index 9337dc62..972f8f6b 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -1,6 +1,7 @@ import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { config } from "@/src/services/configService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { fromStoreItem } from "@/src/services/itemDataService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getRandomInt, getRandomWeightedRewardUc } from "@/src/services/rngService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; @@ -58,7 +59,7 @@ export const dronesController: RequestHandler = async (req, res) => { : 0; const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!; //logger.debug(`drone rolled`, resource); - drone.ResourceType = "/Lotus/" + resource.StoreItem.substring(18); + drone.ResourceType = fromStoreItem(resource.StoreItem); const resourceMeta = ExportResources[drone.ResourceType]; if (resourceMeta.pickupQuantity) { const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity]; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ccfaf0d0..0c5a4cc1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -35,7 +35,7 @@ import { IUpdateChallengeProgressRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { convertInboxMessage, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; +import { convertInboxMessage, fromStoreItem, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; import { EquipmentFeatures, IEquipmentClient, @@ -1260,7 +1260,7 @@ export const addKeyChainItems = async ( `adding key chain items ${keyChainItems.join()} for ${keyChainData.KeyChain} at stage ${keyChainData.ChainStage}` ); - const nonStoreItems = keyChainItems.map(item => item.replace("StoreItems/", "")); + const nonStoreItems = keyChainItems.map(item => fromStoreItem(item)); //TODO: inventoryChanges is not typed correctly const inventoryChanges = {}; diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 8fd3969d..724e0243 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -244,3 +244,11 @@ export const convertInboxMessage = (message: IInboxMessage): IMessage => { r: false } satisfies IMessage; }; + +export const toStoreItem = (type: string): string => { + return "/Lotus/StoreItems/" + type.substring("/Lotus/".length); +}; + +export const fromStoreItem = (type: string): string => { + return "/Lotus/" + type.substring("/Lotus/StoreItems/".length); +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c9986c38..c020e807 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1,4 +1,5 @@ import { + ExportEnemies, ExportFusionBundles, ExportRegions, ExportRewards, @@ -18,6 +19,7 @@ import { addFocusXpIncreases, addFusionTreasures, addGearExpByCategory, + addItem, addMiscItems, addMissionComplete, addMods, @@ -28,7 +30,7 @@ import { import { updateQuestKey } from "@/src/services/questService"; import { HydratedDocument } from "mongoose"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService"; +import { getLevelKeyRewards, getNode, toStoreItem } from "@/src/services/itemDataService"; import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; @@ -319,7 +321,8 @@ export const addMissionRewards = async ( LevelKeyName: levelKeyName, Missions: missions, RegularCredits: creditDrops, - VoidTearParticipantsCurrWave: voidTearWave + VoidTearParticipantsCurrWave: voidTearWave, + StrippedItems: strippedItems }: IMissionInventoryUpdateRequest ) => { if (!rewardInfo) { @@ -406,6 +409,29 @@ export const addMissionRewards = async ( MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount }); } + if (strippedItems) { + for (const si of strippedItems) { + const droptable = ExportEnemies.droptables[si.DropTable]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!droptable) { + logger.error(`unknown droptable ${si.DropTable}`); + } else { + for (let i = 0; i != si.DROP_MOD.length; ++i) { + for (const pool of droptable) { + const reward = getRandomReward(pool.items)!; + logger.debug(`stripped droptable rolled`, reward); + await addItem(inventory, reward.type); + MissionRewards.push({ + StoreItem: toStoreItem(reward.type), + ItemCount: 1, + FromEnemyCache: true // to show "identified" + }); + } + } + } + } + } + return { inventoryChanges, MissionRewards, credits }; }; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 3a84ec4a..b153c86c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -27,6 +27,7 @@ import { } from "warframe-public-export-plus"; import { config } from "./configService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { fromStoreItem, toStoreItem } from "./itemDataService"; export const getStoreItemCategory = (storeItem: string): string => { const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); @@ -240,7 +241,7 @@ export const handleStoreItemAcquisition = async ( await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges); } else { const storeCategory = getStoreItemCategory(storeItemName); - const internalName = storeItemName.replace("/StoreItems", ""); + const internalName = fromStoreItem(storeItemName); logger.debug(`store category ${storeCategory}`); if (!ignorePurchaseQuantity) { if (internalName in ExportGear) { @@ -328,8 +329,7 @@ const handleBoosterPackPurchase = async ( const result = getRandomWeightedRewardUc(pack.components, weights); if (result) { logger.debug(`booster pack rolled`, result); - purchaseResponse.BoosterPackItems += - result.Item.split("/Lotus/").join("/Lotus/StoreItems/") + ',{"lvl":0};'; + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; combineInventoryChanges( purchaseResponse.InventoryChanges, (await addItem(inventory, result.Item, 1)).InventoryChanges diff --git a/src/services/questService.ts b/src/services/questService.ts index 3e036a76..f52536be 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -3,7 +3,7 @@ import { isEmptyObject } from "@/src/helpers/general"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; import { addItem, addKeyChainItems } from "@/src/services/inventoryService"; -import { getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; +import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; import { IInventoryDatabase, IQuestKeyClient, @@ -157,7 +157,7 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); for (const reward of missionRewards) { - await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount); + await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount); } } else if (fixedLevelRewards.levelKeyRewards2) { for (const reward of fixedLevelRewards.levelKeyRewards2) { @@ -166,9 +166,9 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest continue; } if (reward.rewardType == "RT_RESOURCE") { - await addItem(inventory, reward.itemType.replace("StoreItems/", ""), reward.amount); + await addItem(inventory, fromStoreItem(reward.itemType), reward.amount); } else { - await addItem(inventory, reward.itemType.replace("StoreItems/", "")); + await addItem(inventory, fromStoreItem(reward.itemType)); } } } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 441e8d63..c3777112 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -103,6 +103,10 @@ export type IMissionInventoryUpdateRequest = { }[]; CollectibleScans?: ICollectibleEntry[]; Upgrades?: IUpgradeClient[]; // riven challenge progress + StrippedItems?: { + DropTable: string; + DROP_MOD: number[]; + }[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From d5feec2c375b5381075701cd27575797c1d50b52 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 07:43:30 -0700 Subject: [PATCH 128/776] chore: track inventory changes when cracking relic via addMissionRewards (#1131) Closes #1120 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1131 --- src/helpers/relicHelper.ts | 17 ++++++++++++----- src/services/missionInventoryUpdateService.ts | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts index 7c1347d5..13d7d7d4 100644 --- a/src/helpers/relicHelper.ts +++ b/src/helpers/relicHelper.ts @@ -3,12 +3,14 @@ import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; import { getRandomWeightedReward, IRngResult } from "@/src/services/rngService"; import { logger } from "@/src/utils/logger"; -import { addMiscItems } from "@/src/services/inventoryService"; +import { addMiscItems, combineInventoryChanges } from "@/src/services/inventoryService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IInventoryChanges } from "../types/purchaseTypes"; export const crackRelic = async ( inventory: TInventoryDatabaseDocument, - participant: IVoidTearParticipantInfo + participant: IVoidTearParticipantInfo, + inventoryChanges: IInventoryChanges = {} ): Promise => { const relic = ExportRelics[participant.VoidProjection]; const weights = refinementToWeights[relic.quality]; @@ -21,15 +23,20 @@ export const crackRelic = async ( participant.Reward = reward.type; // Remove relic - addMiscItems(inventory, [ + const miscItemChanges = [ { ItemType: participant.VoidProjection, ItemCount: -1 } - ]); + ]; + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); // Give reward - await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount); + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges + ); return reward; }; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c020e807..8fefe1ba 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -405,7 +405,7 @@ export const addMissionRewards = async ( voidTearWave.Participants[0].QualifiesForReward && !voidTearWave.Participants[0].HaveRewardResponse ) { - const reward = await crackRelic(inventory, voidTearWave.Participants[0]); + const reward = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges); MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount }); } From 814f4cfdad8907607c14572f16f328d91e9f4997 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 07:47:24 -0700 Subject: [PATCH 129/776] fix: consume resources & standing required for gilding (#1132) Fixes #1122 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1132 --- src/controllers/api/gildWeaponController.ts | 41 +++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/gildWeaponController.ts b/src/controllers/api/gildWeaponController.ts index ea4f1194..3914b81a 100644 --- a/src/controllers/api/gildWeaponController.ts +++ b/src/controllers/api/gildWeaponController.ts @@ -1,29 +1,29 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory } from "@/src/services/inventoryService"; +import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { WeaponTypeInternal } from "@/src/services/itemDataService"; -import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { ExportRecipes } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [ "LongGuns", "Pistols", "Melee", "OperatorAmps", - "Hoverboards" // Not sure about hoverboards just coppied from modual crafting + "Hoverboards" ]; interface IGildWeaponRequest { ItemName: string; - Recipe: string; // /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint + Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint PolarizeSlot?: number; PolarizeValue?: ArtifactPolarity; ItemId: string; Category: WeaponTypeInternal | "Hoverboards"; } -// In export there no recipes for gild action, so reputation and ressources only consumed visually - export const gildWeaponController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const data = getJSONfromString(String(req.body)); @@ -40,7 +40,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => { } const weapon = inventory[data.Category][weaponIndex]; - weapon.Features = EquipmentFeatures.GILDED; // maybe 9 idk if DOUBLE_CAPACITY is also given + weapon.Features ??= 0; + weapon.Features |= EquipmentFeatures.GILDED; weapon.ItemName = data.ItemName; weapon.XP = 0; if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { @@ -52,11 +53,29 @@ export const gildWeaponController: RequestHandler = async (req, res) => { ]; } inventory[data.Category][weaponIndex] = weapon; - await inventory.save(); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[data.Category] = [weapon.toJSON()]; + const recipe = ExportRecipes[data.Recipe]; + inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ + ItemType: ingredient.ItemType, + ItemCount: ingredient.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); + + const affiliationMods = []; + if (recipe.syndicateStandingChange) { + const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; + affiliation.Standing += recipe.syndicateStandingChange.value; + affiliationMods.push({ + Tag: recipe.syndicateStandingChange.tag, + Standing: recipe.syndicateStandingChange.value + }); + } + + await inventory.save(); res.json({ - InventoryChanges: { - [data.Category]: [weapon] - } + InventoryChanges: inventoryChanges, + AffiliationMods: affiliationMods }); }; From 0ffa9c6bc471aed323c51bb87a3ca7d8a6b5ed0b Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 07:47:32 -0700 Subject: [PATCH 130/776] feat: clan motd (#1134) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1134 --- package-lock.json | 12 ++++++++ package.json | 1 + src/controllers/api/getGuildController.ts | 2 ++ src/controllers/api/setGuildMotdController.ts | 30 +++++++++++++++++++ src/models/guildModel.ts | 13 +++++++- src/routes/api.ts | 3 ++ src/services/loginService.ts | 9 ++++++ src/types/guildTypes.ts | 8 +++++ 8 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/setGuildMotdController.ts diff --git a/package-lock.json b/package-lock.json index 98c06df8..8e58230c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GNU", "dependencies": { "copyfiles": "^2.4.1", + "crc-32": "^1.2.2", "express": "^5", "mongoose": "^8.11.0", "warframe-public-export-plus": "^0.5.42", @@ -1249,6 +1250,17 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", diff --git a/package.json b/package.json index 1a931ddf..dc38de04 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/express": "^5", "@types/morgan": "^1.9.9", "copyfiles": "^2.4.1", + "crc-32": "^1.2.2", "express": "^5", "mongoose": "^8.11.0", "morgan": "^1.10.0", diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 62a27501..e96d7be7 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -26,6 +26,8 @@ const getGuildController: RequestHandler = async (req, res) => { res.json({ _id: toOid(guild._id), Name: guild.Name, + MOTD: guild.MOTD, + LongMOTD: guild.LongMOTD, Members: [ { _id: { $oid: req.query.accountId }, diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts new file mode 100644 index 00000000..32f6f3ec --- /dev/null +++ b/src/controllers/api/setGuildMotdController.ts @@ -0,0 +1,30 @@ +import { Guild } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const setGuildMotdController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString()); + const guild = (await Guild.findOne({ _id: inventory.GuildId! }))!; + // TODO: Check permissions + + const IsLongMOTD = "longMOTD" in req.query; + const MOTD = req.body ? String(req.body) : undefined; + + if (IsLongMOTD) { + if (MOTD) { + guild.LongMOTD = { + message: MOTD, + authorName: getSuffixedName(account) + }; + } else { + guild.LongMOTD = undefined; + } + } else { + guild.MOTD = MOTD ?? ""; + } + await guild.save(); + + res.json({ IsLongMOTD, MOTD }); +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 7afceb98..295d52c5 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -3,7 +3,8 @@ import { IDojoComponentDatabase, ITechProjectDatabase, ITechProjectClient, - IDojoDecoDatabase + IDojoDecoDatabase, + ILongMOTD } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -59,9 +60,19 @@ techProjectSchema.set("toJSON", { } }); +const longMOTDSchema = new Schema( + { + message: String, + authorName: String + }, + { _id: false } +); + const guildSchema = new Schema( { Name: { type: String, required: true }, + MOTD: { type: String, default: "" }, + LongMOTD: { type: longMOTDSchema, default: undefined }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, diff --git a/src/routes/api.ts b/src/routes/api.ts index 964ebd5d..fe2a83d7 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -79,6 +79,7 @@ import { setActiveShipController } from "@/src/controllers/api/setActiveShipCont import { setBootLocationController } from "@/src/controllers/api/setBootLocationController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; +import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; @@ -138,6 +139,7 @@ apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructio apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setBootLocation.php", setBootLocationController); +apiRouter.get("/setGuildMotd.php", setGuildMotdController); apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController); apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController); apiRouter.get("/startLibraryPersonalTarget.php", startLibraryPersonalTargetController); @@ -197,6 +199,7 @@ apiRouter.post("/saveSettings.php", saveSettingsController); apiRouter.post("/sell.php", sellController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); +apiRouter.post("/setGuildMotd.php", setGuildMotdController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 71236f07..08d12ee9 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -8,6 +8,7 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { Request } from "express"; import { config } from "@/src/services/configService"; import { createStats } from "@/src/services/statsService"; +import crc32 from "crc-32"; export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => { return requestPassword === databasePassword; @@ -99,3 +100,11 @@ export const isAdministrator = (account: TAccountDocument): boolean => { } return !!config.administratorNames.find(x => x == account.DisplayName); }; + +const platform_magics = [753, 639, 247, 37, 60]; +export const getSuffixedName = (account: TAccountDocument): string => { + const name = account.DisplayName; + const platformId = 0; + const suffix = ((crc32.str(name.toLowerCase() + "595") >>> 0) + platform_magics[platformId]) % 1000; + return name + "#" + suffix.toString().padStart(3, "0"); +}; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 9effcfbc..8b9fd97b 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -5,6 +5,8 @@ import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTyp export interface IGuildDatabase { _id: Types.ObjectId; Name: string; + MOTD: string; + LongMOTD?: ILongMOTD; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; @@ -27,6 +29,12 @@ export interface IGuildDatabase { CeremonyResetDate?: Date; } +export interface ILongMOTD { + message: string; + authorName: string; + //authorGuildName: ""; +} + export interface IGuildVault { DojoRefundRegularCredits?: number; DojoRefundMiscItems?: IMiscItem[]; From 1ae1cf5170d719f521c64c13d95229de057e0d2c Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 11:15:33 -0700 Subject: [PATCH 131/776] fix: can't rush dojo components with infinitePlatinum (#1138) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1138 --- src/controllers/api/creditsController.ts | 2 +- src/controllers/api/inventoryController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/creditsController.ts b/src/controllers/api/creditsController.ts index c5dea4f7..5fb60575 100644 --- a/src/controllers/api/creditsController.ts +++ b/src/controllers/api/creditsController.ts @@ -19,7 +19,7 @@ export const creditsController: RequestHandler = async (req, res) => { response.RegularCredits = 999999999; } if (config.infinitePlatinum) { - response.PremiumCreditsFree = 999999999; + response.PremiumCreditsFree = 0; response.PremiumCredits = 999999999; } diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 0b33eba4..acb7d2cd 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -70,7 +70,7 @@ export const getInventoryResponse = async ( inventoryResponse.RegularCredits = 999999999; } if (config.infinitePlatinum) { - inventoryResponse.PremiumCreditsFree = 999999999; + inventoryResponse.PremiumCreditsFree = 0; inventoryResponse.PremiumCredits = 999999999; } if (config.infiniteEndo) { From 758135d19bd582737e6c5d8d5d9a58408eeaac4e Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 11:15:45 -0700 Subject: [PATCH 132/776] feat(webui): add resource drones & their blueprints via "add items" (#1137) Closes #1133 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1137 --- .../custom/getItemListsController.ts | 19 +++++++++++++------ src/services/itemDataService.ts | 4 ++++ static/webui/script.js | 5 ++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 6534bf92..8e2b6dc1 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService" import { ExportArcanes, ExportAvionics, + ExportDrones, ExportGear, ExportMisc, ExportRecipes, @@ -80,7 +81,7 @@ const getItemListsController: RequestHandler = (req, response) => { }); if (uniqueName.split("/")[5] != "SentTrainingAmplifier") { res.miscitems.push({ - uniqueName: "MiscItems:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -100,7 +101,7 @@ const getItemListsController: RequestHandler = (req, response) => { } } else if (!item.excludeFromCodex) { res.miscitems.push({ - uniqueName: "MiscItems:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -119,14 +120,14 @@ const getItemListsController: RequestHandler = (req, response) => { } if (uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/") { res.miscitems.push({ - uniqueName: item.productCategory + ":" + uniqueName, + uniqueName: uniqueName, name: name }); } } for (const [uniqueName, item] of Object.entries(ExportRelics)) { res.miscitems.push({ - uniqueName: "MiscItems:" + uniqueName, + uniqueName: uniqueName, name: getString("/Lotus/Language/Relics/VoidProjectionName", lang) .split("|ERA|") @@ -137,7 +138,7 @@ const getItemListsController: RequestHandler = (req, response) => { } for (const [uniqueName, item] of Object.entries(ExportGear)) { res.miscitems.push({ - uniqueName: "Consumables:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -147,12 +148,18 @@ const getItemListsController: RequestHandler = (req, response) => { const resultName = getItemName(item.resultType); if (resultName) { res.miscitems.push({ - uniqueName: "Recipes:" + uniqueName, + uniqueName: uniqueName, name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang)) }); } } } + for (const [uniqueName, item] of Object.entries(ExportDrones)) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } res.mods = []; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 724e0243..416adc7f 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -20,6 +20,7 @@ import { dict_zh, ExportArcanes, ExportCustoms, + ExportDrones, ExportGear, ExportKeys, ExportRecipes, @@ -87,6 +88,9 @@ export const getItemName = (uniqueName: string): string | undefined => { if (uniqueName in ExportCustoms) { return ExportCustoms[uniqueName].name; } + if (uniqueName in ExportDrones) { + return ExportDrones[uniqueName].name; + } if (uniqueName in ExportKeys) { return ExportKeys[uniqueName].name; } diff --git a/static/webui/script.js b/static/webui/script.js index f0190b3c..a03783f2 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -796,12 +796,11 @@ function disposeOfItems(category, type, count) { } function doAcquireMiscItems() { - const data = getKey(document.getElementById("miscitem-type")); - if (!data) { + const uniqueName = getKey(document.getElementById("miscitem-type")); + if (!uniqueName) { $("#miscitem-type").addClass("is-invalid").focus(); return; } - const [category, uniqueName] = data.split(":"); revalidateAuthz(() => { $.post({ url: "/custom/addItems?" + window.authz, From 4937cf7f595df7443599793cf634180a471440d1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 9 Mar 2025 11:16:17 -0700 Subject: [PATCH 133/776] fix: handle refresh request for a single dojo component (#1136) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1136 --- src/controllers/api/getGuildDojoController.ts | 7 ++++++- src/routes/api.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index 04d701be..d03252d8 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -24,5 +24,10 @@ export const getGuildDojoController: RequestHandler = async (req, res) => { await guild.save(); } - res.json(await getDojoClient(guild, 0)); + const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {}; + res.json(await getDojoClient(guild, 0, payload.ComponentId)); }; + +interface IGetGuildDojoRequest { + ComponentId?: string; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index fe2a83d7..0a5e23fe 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -173,6 +173,7 @@ apiRouter.post("/focus.php", focusController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController); apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); +apiRouter.post("/getGuildDojo.php", getGuildDojoController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); From cadb6bc97bbdbd79725b573021116fc5eba4faa7 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 10 Mar 2025 09:15:11 +0100 Subject: [PATCH 134/776] fix: logic error --- src/services/inventoryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 0c5a4cc1..b8e01e53 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -416,7 +416,7 @@ export const addItem = async ( const key = { ItemType: typeName, ItemCount: quantity }; const index = inventory.LevelKeys.findIndex(levelKey => levelKey.ItemType == typeName); - if (index) { + if (index != -1) { inventory.LevelKeys[index].ItemCount += quantity; } else { inventory.LevelKeys.push(key); From b0b52ccabeba7b19342f99a64230a6c42fb62532 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 10 Mar 2025 10:28:52 +0100 Subject: [PATCH 135/776] chore: update package-lock.json --- package-lock.json | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e58230c..7aa704d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,28 +9,28 @@ "version": "0.1.0", "license": "GNU", "dependencies": { + "@types/express": "^5", + "@types/morgan": "^1.9.9", "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "typescript": ">=4.7.4 <5.6.0", "warframe-public-export-plus": "^0.5.42", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@types/express": "^5", - "@types/morgan": "^1.9.9", "@typescript-eslint/eslint-plugin": "^7.18", "@typescript-eslint/parser": "^7.18", "eslint": "^8.56.0", "eslint-plugin-prettier": "^5.2.3", - "morgan": "^1.10.0", "prettier": "^3.4.2", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "tsconfig-paths": "^4.2.0", - "typescript": ">=4.7.4 <5.6.0" + "tsconfig-paths": "^4.2.0" }, "engines": { "node": ">=18.15.0", @@ -338,7 +338,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -349,7 +348,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -359,7 +357,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -372,7 +369,6 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -385,21 +381,18 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/morgan": { "version": "1.9.9", "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -409,7 +402,6 @@ "version": "22.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.9.tgz", "integrity": "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -419,21 +411,18 @@ "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -444,7 +433,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -835,7 +823,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" @@ -848,7 +835,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { @@ -2745,7 +2731,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dev": true, "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", @@ -2762,7 +2747,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -2772,14 +2756,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/morgan/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -2888,7 +2870,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -4019,7 +4000,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4032,7 +4012,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { From 29275fcfdd033eceef3de95d1a1b29b5f283691c Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Mon, 10 Mar 2025 12:02:32 -0700 Subject: [PATCH 136/776] feat(WebUI): German translation (#1142) --- static/webui/script.js | 2 +- static/webui/translations/de.js | 136 ++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 static/webui/translations/de.js diff --git a/static/webui/script.js b/static/webui/script.js index a03783f2..1f831512 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -128,7 +128,7 @@ function setActiveLanguage(lang) { document.querySelector("[data-lang=" + lang + "]").classList.add("active"); window.dictPromise = new Promise(resolve => { - const webui_lang = ["en", "ru", "fr"].indexOf(lang) == -1 ? "en" : lang; + const webui_lang = ["en", "ru", "fr", "de"].indexOf(lang) == -1 ? "en" : lang; let script = document.getElementById("translations"); if (script) document.documentElement.removeChild(script); diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js new file mode 100644 index 00000000..74ec43f4 --- /dev/null +++ b/static/webui/translations/de.js @@ -0,0 +1,136 @@ +// German translation by Animan8000 +dict = { + general_inventoryUpdateNote: `Hinweis: Änderungen, die hier vorgenommen werden, werden erst im Spiel angewendet, sobald das Inventar synchronisiert wird. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, + general_addButton: `Hinzufügen`, + general_bulkActions: `Massenaktionen`, + code_nonValidAuthz: `Deine Anmeldedaten sind nicht mehr gültig.`, + code_changeNameConfirm: `In welchen Namen möchtest du deinen Account umbenennen?`, + code_deleteAccountConfirm: `Bist du sicher, dass du deinen Account |DISPLAYNAME| (|EMAIL|) löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.`, + code_archgun: `Arch-Gewehr`, + code_melee: `Nahkampf`, + code_pistol: `Pistole`, + code_rifle: `Gewehr`, + code_shotgun: `Schrotflinte`, + code_kitgun: `Kitgun`, + code_zaw: `Zaw`, + code_moteAmp: `Anfangsverstärker`, + code_amp: `Verstärker`, + code_sirocco: `Sirocco`, + code_kDrive: `K-Drive`, + code_legendaryCore: `Legendärer Kern`, + code_traumaticPeculiar: `Kuriose Mod: Traumatisch`, + code_starter: `|MOD| (Defekt)`, + code_badItem: `(Fälschung)`, + code_maxRank: `Max. Rang`, + code_rename: `Umbenennen`, + code_renamePrompt: `Neuen benutzerdefinierten Namen eingeben:`, + code_remove: `Entfernen`, + code_addItemsConfirm: `Bist du sicher, dass du |COUNT| Gegenstände zu deinem Account hinzufügen möchtest?`, + code_noEquipmentToRankUp: `Keine Ausstattung zum Rangaufstieg verfügbar.`, + code_succAdded: `Erfolgreich hinzugefügt.`, + code_buffsNumber: `Anzahl der Buffs`, + code_cursesNumber: `Anzahl der Flüche`, + code_rerollsNumber: `Anzahl der Umrollversuche`, + code_viewStats: `Statistiken anzeigen`, + code_rank: `Rang`, + code_count: `Anzahl`, + code_focusAllUnlocked: `Alle Fokus-Schulen sind bereits freigeschaltet.`, + code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, + code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`, + code_succImport: `Erfolgreich importiert.`, + login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, + login_emailLabel: `E-Mail-Adresse`, + login_passwordLabel: `Passwort`, + login_loginButton: `Anmelden`, + navbar_logout: `Abmelden`, + navbar_renameAccount: `Account umbenennen`, + navbar_deleteAccount: `Account löschen`, + navbar_inventory: `Inventar`, + navbar_mods: `Mods`, + navbar_quests: `Quests`, + navbar_cheats: `Cheats`, + navbar_import: `Importieren`, + inventory_addItems: `Gegenstände hinzufügen`, + inventory_suits: `Warframes`, + inventory_longGuns: `Primärwaffen`, + inventory_pistols: `Sekundärwaffen`, + inventory_melee: `Nahkampfwaffen`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Archwing Primärwaffen`, + inventory_spaceMelee: `Archwing Nahkampfwaffen`, + inventory_mechSuits: `Necramechs`, + inventory_sentinels: `Wächter`, + inventory_sentinelWeapons: `Wächter-Waffen`, + inventory_operatorAmps: `Verstärker`, + inventory_hoverboards: `K-Drives`, + inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, + inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, + inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, + inventory_bulkAddSpaceWeapons: `Fehlende Archwing-Waffen hinzufügen`, + inventory_bulkAddSentinels: `Fehlende Wächter hinzufügen`, + inventory_bulkAddSentinelWeapons: `Fehlende Wächter-Waffen hinzufügen`, + inventory_bulkRankUpSuits: `Alle Warframes auf Max. Rang`, + inventory_bulkRankUpWeapons: `Alle Waffen auf Max. Rang`, + inventory_bulkRankUpSpaceSuits: `Alle Archwings auf Max. Rang`, + inventory_bulkRankUpSpaceWeapons: `Alle Archwing-Waffen auf Max. Rang`, + inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`, + inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`, + + currency_RegularCredits: `Credits`, + currency_PremiumCredits: `Platinum`, + currency_FusionPoints: `Endo`, + currency_PrimeTokens: `Reines Aya`, + currency_owned: `Du hast |COUNT|.`, + powersuit_archonShardsLabel: `Archon-Scherben-Slots`, + powersuit_archonShardsDescription: `Du kannst diese unbegrenzten Slots nutzen, um eine Vielzahl von Verbesserungen anzuwenden.`, + mods_addRiven: `Riven hinzufügen`, + mods_fingerprint: `Fingerabdruck`, + mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, + mods_rivens: `Rivens`, + mods_mods: `Mods`, + mods_bulkAddMods: `Fehlende Mods hinzufügen`, + cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge |DISPLAYNAME| zu administratorNames in der config.json hinzu.`, + cheats_server: `Server`, + cheats_skipTutorial: `Tutorial überspringen`, + cheats_skipAllDialogue: `Alle Dialoge überspringen`, + cheats_unlockAllScans: `Alle Scans freischalten`, + cheats_unlockAllMissions: `Alle Missionen freischalten`, + cheats_infiniteCredits: `Unendlich Credits`, + cheats_infinitePlatinum: `Unendlich Platinum`, + cheats_infiniteEndo: `Unendlich Endo`, + cheats_infiniteRegalAya: `Unendlich Reines Aya`, + cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`, + cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`, + cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`, + cheats_unlockAllFlavourItems: `Alle Sammlerstücke freischalten`, + cheats_unlockAllSkins: `Alle Skins freischalten`, + cheats_unlockAllCapturaScenes: `Alle Photora-Szenen freischalten`, + cheats_universalPolarityEverywhere: `Universelle Polarität überall`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`, + cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, + cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, + cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, + cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, + cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, + cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, + cheats_noDojoResearchCosts: `Keine Dojo-Forschungskosten`, + cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`, + cheats_fastClanAscension: `Schneller Clan-Aufstieg`, + cheats_spoofMasteryRank: `Gefälschter Meisterschaftsrang (-1 zum deaktivieren)`, + cheats_saveSettings: `Einstellungen speichern`, + cheats_account: `Account`, + cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`, + cheats_helminthUnlockAll: `Helminth vollständig aufleveln`, + cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`, + cheats_changeButton: `Ändern`, + cheats_none: `Keines`, + cheats_quests: `Quests`, + cheats_quests_unlockAll: `Alle Quests freischalten`, + cheats_quests_completeAll: `Alle Quests abschließen`, + cheats_quests_completeAllUnlocked: `Alle freigeschalteten Quests abschließen`, + cheats_quests_resetAll: `Alle Quests zurücksetzen`, + cheats_quests_giveAll: `Alle Quests erhalten`, + import_importNote: `Du kannst hier eine vollständige oder teilweise Inventarantwort (Client-Darstellung) einfügen. Alle Felder, die vom Importer unterstützt werden, werden in deinem Account überschrieben.`, + import_submit: `Absenden`, + prettier_sucks_ass: `` +}; From b553097fe47fc95e22f2c464c7ed6c6c609a287f Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 10 Mar 2025 16:22:02 -0700 Subject: [PATCH 137/776] fix: handle quest completion via missionInventoryUpdate (#1140) Partial fix for #1126 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1140 Co-authored-by: Sainan Co-committed-by: Sainan --- src/controllers/api/updateQuestController.ts | 19 ++------------ .../custom/manageQuestsController.ts | 2 +- src/services/missionInventoryUpdateService.ts | 2 +- src/services/questService.ts | 25 ++++++++++++++++--- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/controllers/api/updateQuestController.ts b/src/controllers/api/updateQuestController.ts index 3a91ada0..767528d7 100644 --- a/src/controllers/api/updateQuestController.ts +++ b/src/controllers/api/updateQuestController.ts @@ -1,10 +1,8 @@ import { RequestHandler } from "express"; import { parseString } from "@/src/helpers/general"; -import { logger } from "@/src/utils/logger"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService"; -import { getQuestCompletionItems } from "@/src/services/itemDataService"; -import { addItems, getInventory } from "@/src/services/inventoryService"; +import { getInventory } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -22,20 +20,7 @@ export const updateQuestController: RequestHandler = async (req, res) => { const updateQuestResponse: { CustomData?: string; InventoryChanges?: IInventoryChanges; MissionRewards: [] } = { MissionRewards: [] }; - updateQuestKey(inventory, updateQuestRequest.QuestKeys); - - if (updateQuestRequest.QuestKeys[0].Completed) { - logger.debug(`completed quest ${updateQuestRequest.QuestKeys[0].ItemType} `); - const questKeyName = updateQuestRequest.QuestKeys[0].ItemType; - const questCompletionItems = getQuestCompletionItems(questKeyName); - logger.debug(`quest completion items`, questCompletionItems); - - if (questCompletionItems) { - const inventoryChanges = await addItems(inventory, questCompletionItems); - updateQuestResponse.InventoryChanges = inventoryChanges; - } - inventory.ActiveQuest = ""; - } + updateQuestResponse.InventoryChanges = await updateQuestKey(inventory, updateQuestRequest.QuestKeys); //TODO: might need to parse the custom data and add the associated items to inventory if (updateQuestRequest.QuestKeys[0].CustomData) { diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 6899b76d..2234ec00 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -28,7 +28,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => { switch (operation) { case "updateKey": { //TODO: if this is intended to be used, one needs to add a updateQuestKeyMultiple, the game does never intend to do it, so it errors for multiple keys. - updateQuestKey(inventory, questKeyUpdate); + await updateQuestKey(inventory, questKeyUpdate); break; } case "unlockAll": { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 8fefe1ba..71c2828e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -95,7 +95,7 @@ export const addMissionInventoryUpdates = async ( inventory.RegularCredits += value; break; case "QuestKeys": - updateQuestKey(inventory, value); + await updateQuestKey(inventory, value); break; case "AffiliationChanges": updateSyndicate(inventory, value); diff --git a/src/services/questService.ts b/src/services/questService.ts index f52536be..5e70f502 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -2,8 +2,13 @@ import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredIte import { isEmptyObject } from "@/src/helpers/general"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; -import { addItem, addKeyChainItems } from "@/src/services/inventoryService"; -import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; +import { addItem, addItems, addKeyChainItems } from "@/src/services/inventoryService"; +import { + fromStoreItem, + getKeyChainMessage, + getLevelKeyRewards, + getQuestCompletionItems +} from "@/src/services/itemDataService"; import { IInventoryDatabase, IQuestKeyClient, @@ -25,10 +30,10 @@ export interface IUpdateQuestRequest { DoQuestReward: boolean; } -export const updateQuestKey = ( +export const updateQuestKey = async ( inventory: HydratedDocument, questKeyUpdate: IUpdateQuestRequest["QuestKeys"] -): void => { +): Promise => { if (questKeyUpdate.length > 1) { logger.error(`more than 1 quest key not supported`); throw new Error("more than 1 quest key not supported"); @@ -42,9 +47,21 @@ export const updateQuestKey = ( inventory.QuestKeys[questKeyIndex] = questKeyUpdate[0]; + let inventoryChanges: IInventoryChanges = {}; if (questKeyUpdate[0].Completed) { inventory.QuestKeys[questKeyIndex].CompletionDate = new Date(); + + logger.debug(`completed quest ${questKeyUpdate[0].ItemType} `); + const questKeyName = questKeyUpdate[0].ItemType; + const questCompletionItems = getQuestCompletionItems(questKeyName); + logger.debug(`quest completion items`, questCompletionItems); + + if (questCompletionItems) { + inventoryChanges = await addItems(inventory as TInventoryDatabaseDocument, questCompletionItems); + } + inventory.ActiveQuest = ""; } + return inventoryChanges; }; export const updateQuestStage = ( From 00f6a8bd6de524660474a69c3067db315238659c Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 10 Mar 2025 16:22:38 -0700 Subject: [PATCH 138/776] chore: disable cheats by default (#1139) The config.json.example is now has all cheats/time-savers disabled so it's as faithful as possible. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1139 Co-authored-by: Sainan Co-committed-by: Sainan --- config.json.example | 46 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/config.json.example b/config.json.example index d85b0072..d12270ef 100644 --- a/config.json.example +++ b/config.json.example @@ -10,31 +10,31 @@ "NRS": ["localhost"], "administratorNames": [], "autoCreateAccount": true, - "skipTutorial": true, - "skipAllDialogue": true, - "unlockAllScans": true, - "unlockAllMissions": true, - "infiniteCredits": true, - "infinitePlatinum": true, - "infiniteEndo": true, - "infiniteRegalAya": true, + "skipTutorial": false, + "skipAllDialogue": false, + "unlockAllScans": false, + "unlockAllMissions": false, + "infiniteCredits": false, + "infinitePlatinum": false, + "infiniteEndo": false, + "infiniteRegalAya": false, "infiniteHelminthMaterials": false, - "unlockAllShipFeatures": true, - "unlockAllShipDecorations": true, - "unlockAllFlavourItems": true, - "unlockAllSkins": true, - "unlockAllCapturaScenes": true, - "universalPolarityEverywhere": true, - "unlockDoubleCapacityPotatoesEverywhere": true, - "unlockExilusEverywhere": true, - "unlockArcanesEverywhere": true, - "noDailyStandingLimits": true, + "unlockAllShipFeatures": false, + "unlockAllShipDecorations": false, + "unlockAllFlavourItems": false, + "unlockAllSkins": false, + "unlockAllCapturaScenes": false, + "universalPolarityEverywhere": false, + "unlockDoubleCapacityPotatoesEverywhere": false, + "unlockExilusEverywhere": false, + "unlockArcanesEverywhere": false, + "noDailyStandingLimits": false, "instantResourceExtractorDrones": false, - "noDojoRoomBuildStage": true, - "fastDojoRoomDestruction": true, - "noDojoResearchCosts": true, - "noDojoResearchTime": true, - "fastClanAscension": true, + "noDojoRoomBuildStage": false, + "fastDojoRoomDestruction": false, + "noDojoResearchCosts": false, + "noDojoResearchTime": false, + "fastClanAscension": false, "spoofMasteryRank": -1, "events": { "starDays": true From fae6615df43cdaed28f53e31707e354c57411bef Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 10 Mar 2025 16:40:40 -0700 Subject: [PATCH 139/776] feat: clan members (#1143) Now you can add/remove members and accept/decline invites. Closes #1110 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1143 Co-authored-by: Sainan Co-committed-by: Sainan --- src/controllers/api/addToGuildController.ts | 75 +++++++++++ .../api/confirmGuildInvitationController.ts | 32 +++++ src/controllers/api/createGuildController.ts | 25 ++-- .../api/declineGuildInviteController.ts | 14 ++ src/controllers/api/getGuildController.ts | 70 +--------- .../api/removeFromGuildController.ts | 45 +++++++ .../custom/deleteAccountController.ts | 3 + src/models/guildModel.ts | 16 ++- src/models/inboxModel.ts | 10 +- src/models/inventoryModels/inventoryModel.ts | 2 +- src/routes/api.ts | 8 ++ src/services/guildService.ts | 127 +++++++++++++++++- src/types/guildTypes.ts | 35 +++++ 13 files changed, 375 insertions(+), 87 deletions(-) create mode 100644 src/controllers/api/addToGuildController.ts create mode 100644 src/controllers/api/confirmGuildInvitationController.ts create mode 100644 src/controllers/api/declineGuildInviteController.ts create mode 100644 src/controllers/api/removeFromGuildController.ts diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts new file mode 100644 index 00000000..3c95085c --- /dev/null +++ b/src/controllers/api/addToGuildController.ts @@ -0,0 +1,75 @@ +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { Account } from "@/src/models/loginModel"; +import { fillInInventoryDataForGuildMember } from "@/src/services/guildService"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { IGuildMemberClient } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const addToGuildController: RequestHandler = async (req, res) => { + const payload = JSON.parse(String(req.body)) as IAddToGuildRequest; + + const account = await Account.findOne({ DisplayName: payload.UserName }); + if (!account) { + res.status(400).json("Username does not exist"); + return; + } + + const guild = (await Guild.findOne({ _id: payload.GuildId.$oid }, "Name"))!; + // TODO: Check sender is allowed to send invites for this guild. + + if ( + await GuildMember.exists({ + accountId: account._id, + guildId: payload.GuildId.$oid + }) + ) { + res.status(400).json("User already invited to clan"); + return; + } + + await GuildMember.insertOne({ + accountId: account._id, + guildId: payload.GuildId.$oid, + status: 2 // outgoing invite + }); + + const senderAccount = await getAccountForRequest(req); + const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); + await createMessage(account._id.toString(), [ + { + sndr: getSuffixedName(senderAccount), + msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body", + arg: [ + { + Key: "clan", + Tag: guild.Name + "#000" + } + ], + sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + contextInfo: payload.GuildId.$oid, + highPriority: true, + acceptAction: "GUILD_INVITE", + declineAction: "GUILD_INVITE", + hasAccountAction: true + } + ]); + + const member: IGuildMemberClient = { + _id: { $oid: account._id.toString() }, + DisplayName: account.DisplayName, + Rank: 7, + Status: 2 + }; + await fillInInventoryDataForGuildMember(member); + res.json({ NewMember: member }); +}; + +interface IAddToGuildRequest { + UserName: string; + GuildId: IOid; +} diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts new file mode 100644 index 00000000..070c9c28 --- /dev/null +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -0,0 +1,32 @@ +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const confirmGuildInvitationController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const guildMember = await GuildMember.findOne({ + accountId: accountId, + guildId: req.query.clanId as string + }); + if (guildMember) { + guildMember.status = 0; + await guildMember.save(); + await updateInventoryForConfirmedGuildJoin(accountId, new Types.ObjectId(req.query.clanId as string)); + const guild = (await Guild.findOne({ _id: req.query.clanId as string }))!; + res.json({ + ...(await getGuildClient(guild, accountId)), + InventoryChanges: { + Recipes: [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ] + } + }); + } else { + res.end(); + } +}; diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index 8bc34408..dc0930b2 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -1,8 +1,8 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; -import { Guild } from "@/src/models/guildModel"; +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService"; export const createGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -14,20 +14,15 @@ export const createGuildController: RequestHandler = async (req, res) => { }); await guild.save(); - // Update inventory - const inventory = await Inventory.findOne({ accountOwnerId: accountId }); - if (inventory) { - // Set GuildId - inventory.GuildId = guild._id; + // Create guild member on database + await GuildMember.insertOne({ + accountId: accountId, + guildId: guild._id, + status: 0, + rank: 0 + }); - // Give clan key (TODO: This should only be a blueprint) - inventory.LevelKeys.push({ - ItemType: "/Lotus/Types/Keys/DojoKey", - ItemCount: 1 - }); - - await inventory.save(); - } + await updateInventoryForConfirmedGuildJoin(accountId, guild._id); res.json(guild); }; diff --git a/src/controllers/api/declineGuildInviteController.ts b/src/controllers/api/declineGuildInviteController.ts new file mode 100644 index 00000000..c2bcd073 --- /dev/null +++ b/src/controllers/api/declineGuildInviteController.ts @@ -0,0 +1,14 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const declineGuildInviteController: RequestHandler = async (req, res) => { + const accountId = await getAccountForRequest(req); + + await GuildMember.deleteOne({ + accountId: accountId, + guildId: req.query.clanId as string + }); + + res.end(); +}; diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index e96d7be7..be697530 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -1,18 +1,13 @@ import { RequestHandler } from "express"; -import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Guild } from "@/src/models/guildModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; -import { getGuildVault } from "@/src/services/guildService"; import { logger } from "@/src/utils/logger"; +import { getInventory } from "@/src/services/inventoryService"; +import { getGuildClient } from "@/src/services/guildService"; const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await Inventory.findOne({ accountOwnerId: accountId }); - if (!inventory) { - res.status(400).json({ error: "inventory was undefined" }); - return; - } + const inventory = await getInventory(accountId); if (inventory.GuildId) { const guild = await Guild.findOne({ _id: inventory.GuildId }); if (guild) { @@ -23,64 +18,7 @@ const getGuildController: RequestHandler = async (req, res) => { guild.CeremonyResetDate = undefined; await guild.save(); } - res.json({ - _id: toOid(guild._id), - Name: guild.Name, - MOTD: guild.MOTD, - LongMOTD: guild.LongMOTD, - Members: [ - { - _id: { $oid: req.query.accountId }, - Rank: 0, - Status: 0 - } - ], - Ranks: [ - { - Name: "/Lotus/Language/Game/Rank_Creator", - Permissions: 16351 - }, - { - Name: "/Lotus/Language/Game/Rank_Warlord", - Permissions: 14303 - }, - { - Name: "/Lotus/Language/Game/Rank_General", - Permissions: 4318 - }, - { - Name: "/Lotus/Language/Game/Rank_Officer", - Permissions: 4314 - }, - { - Name: "/Lotus/Language/Game/Rank_Leader", - Permissions: 4106 - }, - { - Name: "/Lotus/Language/Game/Rank_Sage", - Permissions: 4304 - }, - { - Name: "/Lotus/Language/Game/Rank_Soldier", - Permissions: 4098 - }, - { - Name: "/Lotus/Language/Game/Rank_Initiate", - Permissions: 4096 - }, - { - Name: "/Lotus/Language/Game/Rank_Utility", - Permissions: 4096 - } - ], - Tier: 1, - Vault: getGuildVault(guild), - Class: guild.Class, - XP: guild.XP, - IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), - NumContributors: guild.CeremonyContributors?.length ?? 0, - CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined - }); + res.json(await getGuildClient(guild, accountId)); return; } } diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts new file mode 100644 index 00000000..c28700e4 --- /dev/null +++ b/src/controllers/api/removeFromGuildController.ts @@ -0,0 +1,45 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { getGuildForRequest } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { RequestHandler } from "express"; + +export const removeFromGuildController: RequestHandler = async (req, res) => { + const guild = await getGuildForRequest(req); + // TODO: Check permissions + const payload = JSON.parse(String(req.body)) as IRemoveFromGuildRequest; + + const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!; + if (guildMember.status == 0) { + const inventory = await getInventory(payload.userId); + inventory.GuildId = undefined; + + // Remove clan key or blueprint from kicked member + const itemIndex = inventory.MiscItems.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); + if (itemIndex != -1) { + inventory.MiscItems.splice(itemIndex, 1); + } else { + const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); + if (recipeIndex != -1) { + inventory.Recipes.splice(itemIndex, 1); + } + } + + await inventory.save(); + + // TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think) + } else if (guildMember.status == 2) { + // TODO: Maybe the inbox message for the sent invite should be deleted? + } + await GuildMember.deleteOne({ _id: guildMember._id }); + + res.json({ + _id: payload.userId, + ItemToRemove: "/Lotus/Types/Keys/DojoKey", + RecipeToRemove: "/Lotus/Types/Keys/DojoKeyBlueprint" + }); +}; + +interface IRemoveFromGuildRequest { + userId: string; + kicker?: string; +} diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index fb8ca399..cb249d69 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -7,11 +7,14 @@ import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { Ship } from "@/src/models/shipModel"; import { Stats } from "@/src/models/statsModel"; +import { GuildMember } from "@/src/models/guildModel"; export const deleteAccountController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); + // TODO: Handle the account being the creator of a guild await Promise.all([ Account.deleteOne({ _id: accountId }), + GuildMember.deleteOne({ accountId: accountId }), Inbox.deleteMany({ ownerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }), Loadout.deleteOne({ loadoutOwnerId: accountId }), diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 295d52c5..9815d257 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -4,7 +4,8 @@ import { ITechProjectDatabase, ITechProjectClient, IDojoDecoDatabase, - ILongMOTD + ILongMOTD, + IGuildMemberDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -70,7 +71,7 @@ const longMOTDSchema = new Schema( const guildSchema = new Schema( { - Name: { type: String, required: true }, + Name: { type: String, required: true, unique: true }, MOTD: { type: String, default: "" }, LongMOTD: { type: longMOTDSchema, default: undefined }, DojoComponents: { type: [dojoComponentSchema], default: [] }, @@ -113,3 +114,14 @@ export type TGuildDatabaseDocument = Document & keyof GuildDocumentProps > & GuildDocumentProps; + +const guildMemberSchema = new Schema({ + accountId: Types.ObjectId, + guildId: Types.ObjectId, + status: { type: Number, required: true }, + rank: { type: Number, default: 7 } +}); + +guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true }); + +export const GuildMember = model("GuildMember", guildMemberSchema); diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index 10b1930e..c451d18a 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -32,6 +32,10 @@ export interface IMessage { transmission?: string; arg?: Arg[]; r?: boolean; + contextInfo?: string; + acceptAction?: string; + declineAction?: string; + hasAccountAction?: boolean; } export interface Arg { @@ -100,7 +104,11 @@ const messageSchema = new Schema( } ], default: undefined - } + }, + contextInfo: String, + acceptAction: String, + declineAction: String, + hasAccountAction: Boolean }, { timestamps: { createdAt: "date", updatedAt: false }, id: false } ); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 280934b8..f8cf68f2 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1249,7 +1249,7 @@ const inventorySchema = new Schema( Drones: [droneSchema], //Active profile ico - ActiveAvatarImageType: String, + ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" }, // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable DiscoveredMarkers: [Schema.Types.Mixed], diff --git a/src/routes/api.ts b/src/routes/api.ts index 0a5e23fe..75ddf345 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; @@ -14,11 +15,13 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController"; import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; @@ -69,6 +72,7 @@ import { playerSkillsController } from "@/src/controllers/api/playerSkillsContro import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; +import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadout"; @@ -113,7 +117,9 @@ apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController) apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); +apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.get("/credits.php", creditsController); +apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); apiRouter.get("/deleteSession.php", deleteSessionController); apiRouter.get("/dojo", dojoController); apiRouter.get("/drones.php", dronesController); @@ -150,6 +156,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/artifacts.php", artifactsController); @@ -193,6 +200,7 @@ apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); +apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveLoadout.php", saveLoadoutController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 9c89f863..a27c25b2 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,19 +1,23 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; -import { Guild, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { addRecipes, getInventory } from "@/src/services/inventoryService"; +import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IDojoClient, IDojoComponentClient, IDojoContributable, IDojoDecoClient, + IGuildClient, + IGuildMemberClient, IGuildVault } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; +import { config } from "./configService"; +import { Account } from "../models/loginModel"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -36,6 +40,99 @@ export const getGuildForRequestEx = async ( return guild; }; +export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: string): Promise => { + const guildMembers = await GuildMember.find({ guildId: guild._id }); + + const members: IGuildMemberClient[] = []; + let missingEntry = true; + for (const guildMember of guildMembers) { + const member: IGuildMemberClient = { + _id: toOid(guildMember.accountId), + Rank: guildMember.rank, + Status: guildMember.status + }; + if (guildMember.accountId.equals(accountId)) { + missingEntry = false; + } else { + member.DisplayName = (await Account.findOne( + { + _id: guildMember.accountId + }, + "DisplayName" + ))!.DisplayName; + await fillInInventoryDataForGuildMember(member); + } + members.push(member); + } + if (missingEntry) { + // Handle clans created prior to creation of the GuildMember model. + await GuildMember.insertOne({ + accountId: accountId, + guildId: guild._id, + status: 0, + rank: 0 + }); + members.push({ + _id: { $oid: accountId }, + Status: 0, + Rank: 0 + }); + } + + return { + _id: toOid(guild._id), + Name: guild.Name, + MOTD: guild.MOTD, + LongMOTD: guild.LongMOTD, + Members: members, + Ranks: [ + { + Name: "/Lotus/Language/Game/Rank_Creator", + Permissions: 16351 + }, + { + Name: "/Lotus/Language/Game/Rank_Warlord", + Permissions: 14303 + }, + { + Name: "/Lotus/Language/Game/Rank_General", + Permissions: 4318 + }, + { + Name: "/Lotus/Language/Game/Rank_Officer", + Permissions: 4314 + }, + { + Name: "/Lotus/Language/Game/Rank_Leader", + Permissions: 4106 + }, + { + Name: "/Lotus/Language/Game/Rank_Sage", + Permissions: 4304 + }, + { + Name: "/Lotus/Language/Game/Rank_Soldier", + Permissions: 4098 + }, + { + Name: "/Lotus/Language/Game/Rank_Initiate", + Permissions: 4096 + }, + { + Name: "/Lotus/Language/Game/Rank_Utility", + Permissions: 4096 + } + ], + Tier: 1, + Vault: getGuildVault(guild), + Class: guild.Class, + XP: guild.XP, + IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), + NumContributors: guild.CeremonyContributors?.length ?? 0, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined + }; +}; + export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => { return { DojoRefundRegularCredits: guild.VaultRegularCredits, @@ -192,3 +289,29 @@ export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, } } }; + +export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClient): Promise => { + const inventory = await getInventory(member._id.$oid, "PlayerLevel ActiveAvatarImageType"); + member.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank; + member.ActiveAvatarImageType = inventory.ActiveAvatarImageType; +}; + +export const updateInventoryForConfirmedGuildJoin = async ( + accountId: string, + guildId: Types.ObjectId +): Promise => { + const inventory = await getInventory(accountId); + + // Set GuildId + inventory.GuildId = guildId; + + // Give clan key blueprint + addRecipes(inventory, [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]); + + await inventory.save(); +}; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 8b9fd97b..7f241212 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -2,6 +2,25 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "@/src/types/commonTypes"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +export interface IGuildClient { + _id: IOid; + Name: string; + MOTD: string; + LongMOTD?: ILongMOTD; + Members: IGuildMemberClient[]; + Ranks: { + Name: string; + Permissions: number; + }[]; + Tier: number; + Vault: IGuildVault; + Class: number; + XP: number; + IsContributor: boolean; + NumContributors: number; + CeremonyResetDate?: IMongoDate; +} + export interface IGuildDatabase { _id: Types.ObjectId; Name: string; @@ -35,6 +54,22 @@ export interface ILongMOTD { //authorGuildName: ""; } +export interface IGuildMemberDatabase { + accountId: Types.ObjectId; + guildId: Types.ObjectId; + status: number; + rank: number; +} + +export interface IGuildMemberClient { + _id: IOid; + Status: number; + Rank: number; + DisplayName?: string; + ActiveAvatarImageType?: string; + PlayerLevel?: number; +} + export interface IGuildVault { DojoRefundRegularCredits?: number; DojoRefundMiscItems?: IMiscItem[]; From ead7b67efc8b2a2d96220339a233225c5d0c2d78 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 11 Mar 2025 02:04:25 -0700 Subject: [PATCH 140/776] chore: remove ts-node dependency (#1148) We either use ts-node-dev or compile to JS and then run that so this isn't needed. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1148 --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7aa704d0..d7524f05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "eslint": "^8.56.0", "eslint-plugin-prettier": "^5.2.3", "prettier": "^3.4.2", - "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0" }, diff --git a/package.json b/package.json index dc38de04..1a824fd1 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "eslint": "^8.56.0", "eslint-plugin-prettier": "^5.2.3", "prettier": "^3.4.2", - "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0" }, From 38dfe14776710592a2a9fd3b6c41dad8460d42fe Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 11 Mar 2025 07:56:18 -0700 Subject: [PATCH 141/776] feat: fabricate research (#1150) Closes #910 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1150 --- package-lock.json | 8 ++-- package.json | 2 +- src/controllers/api/guildTechController.ts | 39 ++++++++++++++++--- .../custom/getItemListsController.ts | 7 ++++ src/models/inventoryModels/inventoryModel.ts | 25 +++++++++++- src/services/inventoryService.ts | 29 +++++++++++++- src/types/inventoryTypes/inventoryTypes.ts | 21 ++++++++-- 7 files changed, 115 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index d7524f05..486ac1a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=4.7.4 <5.6.0", - "warframe-public-export-plus": "^0.5.42", + "warframe-public-export-plus": "^0.5.43", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4073,9 +4073,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.42", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.42.tgz", - "integrity": "sha512-up3P5bLKD42Xkr3o7TX9WUwvpJzK88aQTLZ2bB6QWUHdsJxl/Z3TBn+HSd3eouIDTMVUzbTDeDPosSw7TcLegA==" + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.43.tgz", + "integrity": "sha512-LeF7HmsjOPsJDtgr66x3iMEIAQgcxKNM54VG895FTemgHLLo34UGDyeS1yIfY67WxxbTUgW3MkHQLlCEJXD14w==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 1a824fd1..eb4ceaa7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=4.7.4 <5.6.0", - "warframe-public-export-plus": "^0.5.42", + "warframe-public-export-plus": "^0.5.43", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 35dffe2d..90714492 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -2,7 +2,14 @@ import { RequestHandler } from "express"; import { getGuildForRequestEx, getGuildVault, scaleRequiredCount } from "@/src/services/guildService"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { + addItem, + addMiscItems, + addRecipes, + combineInventoryChanges, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; @@ -127,6 +134,20 @@ export const guildTechController: RequestHandler = async (req, res) => { Recipes: recipeChanges } }); + } else if (action == "Fabricate") { + const payload = data as IGuildTechFabricateRequest; + const recipe = ExportDojoRecipes.fabrications[payload.RecipeType]; + const inventory = await getInventory(accountId); + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false); + inventoryChanges.MiscItems = recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: x.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); + combineInventoryChanges(inventoryChanges, (await addItem(inventory, recipe.resultType)).InventoryChanges); + await inventory.save(); + // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. + res.json({ inventoryChanges: inventoryChanges }); } else { throw new Error(`unknown guildTech action: ${data.Action}`); } @@ -144,10 +165,12 @@ const processFundedProject = ( } }; -type TGuildTechRequest = { - Action: string; -} & Partial & - Partial; +type TGuildTechRequest = + | ({ + Action: string; + } & Partial & + Partial) + | IGuildTechFabricateRequest; interface IGuildTechStartFields { Mode: "Guild"; @@ -164,3 +187,9 @@ interface IGuildTechContributeFields { VaultCredits: number; VaultMiscItems: IMiscItem[]; } + +interface IGuildTechFabricateRequest { + Action: "Fabricate"; + Mode: "Guild"; + RecipeType: string; +} diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 8e2b6dc1..cfdde03d 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -6,6 +6,7 @@ import { ExportDrones, ExportGear, ExportMisc, + ExportRailjackWeapons, ExportRecipes, ExportRelics, ExportResources, @@ -160,6 +161,12 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(item.name, lang) }); } + for (const [uniqueName, item] of Object.entries(ExportRailjackWeapons)) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } res.mods = []; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index f8cf68f2..880dd09e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -75,7 +75,8 @@ import { ICollectibleEntry, IIncentiveState, ISongChallenge, - ILibraryPersonalProgress + ILibraryPersonalProgress, + ICrewShipWeaponDatabase } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1037,6 +1038,25 @@ const alignmentSchema = new Schema( { _id: false } ); +const crewShipWeaponSchema2 = new Schema( + { + ItemType: String + }, + { id: false } +); + +crewShipWeaponSchema2.virtual("ItemId").get(function () { + return { $oid: this._id.toString() } satisfies IOid; +}); + +crewShipWeaponSchema2.set("toJSON", { + virtuals: true, + transform(_document, returnedObject) { + delete returnedObject._id; + delete returnedObject.__v; + } +}); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1157,7 +1177,7 @@ const inventorySchema = new Schema( //Default RailJack CrewShipAmmo: [typeCountSchema], - CrewShipWeapons: [Schema.Types.Mixed], + CrewShipWeapons: [crewShipWeaponSchema2], CrewShipWeaponSkins: [upgradeSchema], //NPC Crew and weapon @@ -1404,6 +1424,7 @@ export type InventoryDocumentProps = { WeaponSkins: Types.DocumentArray; QuestKeys: Types.DocumentArray; Drones: Types.DocumentArray; + CrewShipWeapons: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index b8e01e53..59da757a 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -26,7 +26,8 @@ import { ILibraryDailyTaskInfo, ICalendarProgress, IDroneClient, - IUpgradeClient + IUpgradeClient, + ICrewShipWeaponClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate } from "../types/genericUpdate"; import { @@ -54,6 +55,7 @@ import { ExportGear, ExportKeys, ExportMisc, + ExportRailjackWeapons, ExportRecipes, ExportResources, ExportSentinels, @@ -386,6 +388,14 @@ export const addItem = async ( }; } } + if (typeName in ExportRailjackWeapons) { + return { + InventoryChanges: { + ...addCrewShipWeapon(inventory, typeName), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) + } + }; + } if (typeName in ExportMisc.creditBundles) { const creditsTotal = ExportMisc.creditBundles[typeName] * quantity; inventory.RegularCredits += creditsTotal; @@ -859,6 +869,7 @@ export const addCustomization = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.FlavourItems ??= []; (inventoryChanges.FlavourItems as IFlavourItem[]).push( inventory.FlavourItems[flavourItemIndex].toJSON() @@ -872,6 +883,7 @@ export const addSkin = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { const index = inventory.WeaponSkins.push({ ItemType: typeName }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.WeaponSkins ??= []; (inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push( inventory.WeaponSkins[index].toJSON() @@ -879,12 +891,27 @@ export const addSkin = ( return inventoryChanges; }; +const addCrewShipWeapon = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = inventory.CrewShipWeapons.push({ ItemType: typeName, _id: new Types.ObjectId() }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.CrewShipWeapons ??= []; + (inventoryChanges.CrewShipWeapons as ICrewShipWeaponClient[]).push( + inventory.CrewShipWeapons[index].toJSON() + ); + return inventoryChanges; +}; + const addCrewShipWeaponSkin = ( inventory: TInventoryDatabaseDocument, typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.CrewShipWeaponSkins ??= []; (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push( inventory.CrewShipWeaponSkins[index].toJSON() diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 22f80b99..af029a34 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -31,6 +31,7 @@ export interface IInventoryDatabase | "WeaponSkins" | "Upgrades" | "CrewShipSalvagedWeaponSkins" + | "CrewShipWeapons" | "CrewShipWeaponSkins" | "AdultOperatorLoadOuts" | "OperatorLoadOuts" @@ -56,6 +57,7 @@ export interface IInventoryDatabase WeaponSkins: IWeaponSkinDatabase[]; Upgrades: IUpgradeDatabase[]; CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; + CrewShipWeapons: ICrewShipWeaponDatabase[]; CrewShipWeaponSkins: IUpgradeDatabase[]; AdultOperatorLoadOuts: IOperatorConfigDatabase[]; OperatorLoadOuts: IOperatorConfigDatabase[]; @@ -289,8 +291,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu PlayerSkills: IPlayerSkills; CrewShipAmmo: IConsumable[]; CrewShipSalvagedWeaponSkins: IUpgradeClient[]; - CrewShipWeapons: ICrewShipWeapon[]; - CrewShipSalvagedWeapons: ICrewShipWeapon[]; + CrewShipWeapons: ICrewShipWeaponClient[]; + CrewShipSalvagedWeapons: IEquipmentClient[]; CrewShipWeaponSkins: IUpgradeClient[]; TradeBannedUntil?: IMongoDate; PlayedParkourTutorial: boolean; @@ -428,7 +430,8 @@ export enum InventorySlot { SPACESUITS = "SpaceSuitBin", MECHSUITS = "MechBin", PVE_LOADOUTS = "PveBonusLoadoutBin", - SENTINELS = "SentinelBin" + SENTINELS = "SentinelBin", + RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin" } export interface ISlots { @@ -489,11 +492,23 @@ export interface IFlavourItem { export type IMiscItem = ITypeCount; +// inventory.CrewShips[0].Weapon export interface ICrewShipWeapon { PILOT: ICrewShipPilotWeapon; PORT_GUNS: ICrewShipPortGuns; } +// inventory.CrewShipWeapons +export interface ICrewShipWeaponClient { + ItemType: string; + ItemId: IOid; +} + +export interface ICrewShipWeaponDatabase { + ItemType: string; + _id: Types.ObjectId; +} + export interface ICrewShipPilotWeapon { PRIMARY_A: IEquipmentSelection; SECONDARY_A: IEquipmentSelection; From 1b54bcd1e0879b1ad10b215e23e47611a7a00fbe Mon Sep 17 00:00:00 2001 From: Mebius Date: Tue, 11 Mar 2025 09:13:43 -0700 Subject: [PATCH 142/776] feat(webui): Chinese translation (#1154) Co-authored-by: Belenus Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1154 --- static/webui/script.js | 2 +- static/webui/translations/zh.js | 136 ++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 static/webui/translations/zh.js diff --git a/static/webui/script.js b/static/webui/script.js index 1f831512..012388ea 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -128,7 +128,7 @@ function setActiveLanguage(lang) { document.querySelector("[data-lang=" + lang + "]").classList.add("active"); window.dictPromise = new Promise(resolve => { - const webui_lang = ["en", "ru", "fr", "de"].indexOf(lang) == -1 ? "en" : lang; + const webui_lang = ["en", "ru", "fr", "de", "zh"].indexOf(lang) == -1 ? "en" : lang; let script = document.getElementById("translations"); if (script) document.documentElement.removeChild(script); diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js new file mode 100644 index 00000000..78c167ba --- /dev/null +++ b/static/webui/translations/zh.js @@ -0,0 +1,136 @@ +// Chinese translation by meb154 +dict = { + general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`, + general_addButton: `添加`, + general_bulkActions: `批量操作`, + code_nonValidAuthz: `您的登录凭证已失效。`, + code_changeNameConfirm: `您想将账户名称更改为什么?`, + code_deleteAccountConfirm: `确定要删除账户 |DISPLAYNAME| (|EMAIL|) 吗?此操作不可撤销。`, + code_archgun: `空战`, + code_melee: `近战`, + code_pistol: `手枪`, + code_rifle: `步枪`, + code_shotgun: `霰弹枪`, + code_kitgun: `组合枪`, + code_zaw: `自制近战`, + code_moteAmp: `微尘增幅器`, + code_amp: `增幅器`, + code_sirocco: `赤风`, + code_kDrive: `K式悬浮板`, + code_legendaryCore: `传奇核心`, + code_traumaticPeculiar: `Traumatic Peculiar`, + code_starter: `|MOD| (有瑕疵的)`, + code_badItem: `(Imposter)`, + code_maxRank: `满级`, + code_rename: `重命名`, + code_renamePrompt: `输入新的自定义名称:`, + code_remove: `移除`, + code_addItemsConfirm: `确定要向账户添加 |COUNT| 件物品吗?`, + code_noEquipmentToRankUp: `没有可升级的装备。`, + code_succAdded: `已成功添加。`, + code_buffsNumber: `增益数量`, + code_cursesNumber: `负面数量`, + code_rerollsNumber: `洗卡次数`, + code_viewStats: `查看属性`, + code_rank: `等级`, + code_count: `数量`, + code_focusAllUnlocked: `所有专精学派均已解锁。`, + code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, + code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, + code_succImport: `导入成功。`, + login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, + login_emailLabel: `电子邮箱`, + login_passwordLabel: `密码`, + login_loginButton: `登录`, + navbar_logout: `退出登录`, + navbar_renameAccount: `重命名账户`, + navbar_deleteAccount: `删除账户`, + navbar_inventory: `仓库`, + navbar_mods: `Mods`, + navbar_quests: `任务`, + navbar_cheats: `作弊选项`, + navbar_import: `导入`, + inventory_addItems: `添加物品`, + inventory_suits: `战甲`, + inventory_longGuns: `主要武器`, + inventory_pistols: `次要武器`, + inventory_melee: `近战武器`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Archwing主武器`, + inventory_spaceMelee: `Archwing近战武器`, + inventory_mechSuits: `殁世机甲`, + inventory_sentinels: `守护`, + inventory_sentinelWeapons: `守护武器`, + inventory_operatorAmps: `增幅器`, + inventory_hoverboards: `K式悬浮板`, + inventory_bulkAddSuits: `添加缺失战甲`, + inventory_bulkAddWeapons: `添加缺失武器`, + inventory_bulkAddSpaceSuits: `添加缺失Archwing`, + inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`, + inventory_bulkAddSentinels: `添加缺失守护`, + inventory_bulkAddSentinelWeapons: `添加缺失守护武器`, + inventory_bulkRankUpSuits: `所有战甲升满级`, + inventory_bulkRankUpWeapons: `所有武器升满级`, + inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`, + inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`, + inventory_bulkRankUpSentinels: `所有守护升满级`, + inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`, + + currency_RegularCredits: `现金`, + currency_PremiumCredits: `白金`, + currency_FusionPoints: `内融核心`, + currency_PrimeTokens: `御品阿耶`, + currency_owned: `当前拥有 |COUNT|。`, + powersuit_archonShardsLabel: `执刑官源力石槽位`, + powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`, + mods_addRiven: `添加裂罅MOD`, + mods_fingerprint: `印记`, + mods_fingerprintHelp: `需要印记相关的帮助?`, + mods_rivens: `裂罅MOD`, + mods_mods: `Mods`, + mods_bulkAddMods: `添加缺失MOD`, + cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 |DISPLAYNAME| 添加到 config.json 的 administratorNames 中。`, + cheats_server: `服务器`, + cheats_skipTutorial: `跳过教程`, + cheats_skipAllDialogue: `跳过所有对话`, + cheats_unlockAllScans: `解锁所有扫描`, + cheats_unlockAllMissions: `解锁所有任务`, + cheats_infiniteCredits: `无限现金`, + cheats_infinitePlatinum: `无限白金`, + cheats_infiniteEndo: `无限内融核心`, + cheats_infiniteRegalAya: `无限御品阿耶`, + cheats_infiniteHelminthMaterials: `无限Helminth材料`, + cheats_unlockAllShipFeatures: `解锁所有飞船功能`, + cheats_unlockAllShipDecorations: `解锁所有飞船装饰`, + cheats_unlockAllFlavourItems: `解锁所有装饰物品`, + cheats_unlockAllSkins: `解锁所有外观`, + cheats_unlockAllCapturaScenes: `解锁所有Captura场景`, + cheats_universalPolarityEverywhere: `全局万用极性`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`, + cheats_unlockExilusEverywhere: `全物品自带适配器`, + cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, + cheats_noDailyStandingLimits: `无每日声望限制`, + cheats_instantResourceExtractorDrones: `即时资源采集无人机`, + cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, + cheats_fastDojoRoomDestruction: `快速拆除道场房间`, + cheats_noDojoResearchCosts: `无视道场研究消耗`, + cheats_noDojoResearchTime: `无视道场研究时间`, + cheats_fastClanAscension: `快速升级氏族`, + cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`, + cheats_saveSettings: `保存设置`, + cheats_account: `账户`, + cheats_unlockAllFocusSchools: `解锁所有专精学派`, + cheats_helminthUnlockAll: `完全升级Helminth`, + cheats_changeSupportedSyndicate: `支持的集团`, + cheats_changeButton: `更改`, + cheats_none: `无`, + cheats_quests: `任务`, + cheats_quests_unlockAll: `解锁所有任务`, + cheats_quests_completeAll: `完成所有任务`, + cheats_quests_completeAllUnlocked: `完成所有已解锁任务`, + cheats_quests_resetAll: `重置所有任务`, + cheats_quests_giveAll: `授予所有任务`, + import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段将被覆盖到您的账户中。`, + import_submit: `提交`, + prettier_sucks_ass: `` +}; From d24aac2ab27f49f836c7903f3477c6315664e5e2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 11 Mar 2025 10:31:56 -0700 Subject: [PATCH 143/776] feat: clan name discriminators (#1147) Closes #1145 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1147 --- src/controllers/api/addToGuildController.ts | 2 +- src/controllers/api/createGuildController.ts | 10 +++++++--- src/controllers/api/getGuildController.ts | 8 +++++++- src/services/guildService.ts | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index 3c95085c..a7a3f250 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -46,7 +46,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { arg: [ { Key: "clan", - Tag: guild.Name + "#000" + Tag: guild.Name } ], sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index dc0930b2..cef7e423 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -2,7 +2,11 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Guild, GuildMember } from "@/src/models/guildModel"; -import { updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService"; +import { + createUniqueClanName, + getGuildClient, + updateInventoryForConfirmedGuildJoin +} from "@/src/services/guildService"; export const createGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -10,7 +14,7 @@ export const createGuildController: RequestHandler = async (req, res) => { // Create guild on database const guild = new Guild({ - Name: payload.guildName + Name: await createUniqueClanName(payload.guildName) }); await guild.save(); @@ -24,7 +28,7 @@ export const createGuildController: RequestHandler = async (req, res) => { await updateInventoryForConfirmedGuildJoin(accountId, guild._id); - res.json(guild); + res.json(await getGuildClient(guild, accountId)); }; interface ICreateGuildRequest { diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index be697530..9dcfcbf1 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -3,7 +3,7 @@ import { Guild } from "@/src/models/guildModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { logger } from "@/src/utils/logger"; import { getInventory } from "@/src/services/inventoryService"; -import { getGuildClient } from "@/src/services/guildService"; +import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -11,6 +11,12 @@ const getGuildController: RequestHandler = async (req, res) => { if (inventory.GuildId) { const guild = await Guild.findOne({ _id: inventory.GuildId }); if (guild) { + // Handle guilds created before we added discriminators + if (guild.Name.indexOf("#") == -1) { + guild.Name = await createUniqueClanName(guild.Name); + await guild.save(); + } + if (guild.CeremonyResetDate && Date.now() >= guild.CeremonyResetDate.getTime()) { logger.debug(`ascension ceremony is over`); guild.CeremonyEndo = undefined; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index a27c25b2..3d53a211 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -18,6 +18,7 @@ import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; import { config } from "./configService"; import { Account } from "../models/loginModel"; +import { getRandomInt } from "./rngService"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -315,3 +316,16 @@ export const updateInventoryForConfirmedGuildJoin = async ( await inventory.save(); }; + +export const createUniqueClanName = async (name: string): Promise => { + const initialDiscriminator = getRandomInt(0, 999); + let discriminator = initialDiscriminator; + do { + const fullName = name + "#" + discriminator.toString().padStart(3, "0"); + if (!(await Guild.exists({ Name: fullName }))) { + return fullName; + } + discriminator = (discriminator + 1) % 1000; + } while (discriminator != initialDiscriminator); + throw new Error(`clan name is so unoriginal it's already been done 1000 times: ${name}`); +}; From 4e0494f15d223b52bcc97ae80a43d9b7e3948d45 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 11 Mar 2025 10:32:44 -0700 Subject: [PATCH 144/776] fix: ignore purchaseQuantity when getting slots via a bundle (#1151) Fixes #1149 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1151 --- src/services/purchaseService.ts | 35 +++++++++++++++++++-------------- src/types/purchaseTypes.ts | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index b153c86c..19e683fe 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -256,7 +256,7 @@ export const handleStoreItemAcquisition = async ( break; } case "Types": - purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity); + purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity); break; case "Boosters": purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); @@ -267,16 +267,16 @@ export const handleStoreItemAcquisition = async ( }; export const slotPurchaseNameToSlotName: SlotPurchase = { - SuitSlotItem: { name: "SuitBin", slotsPerPurchase: 1 }, - TwoSentinelSlotItem: { name: "SentinelBin", slotsPerPurchase: 2 }, - TwoWeaponSlotItem: { name: "WeaponBin", slotsPerPurchase: 2 }, - SpaceSuitSlotItem: { name: "SpaceSuitBin", slotsPerPurchase: 1 }, - TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", slotsPerPurchase: 2 }, - MechSlotItem: { name: "MechBin", slotsPerPurchase: 1 }, - TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", slotsPerPurchase: 2 }, - RandomModSlotItem: { name: "RandomModBin", slotsPerPurchase: 3 }, - TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", slotsPerPurchase: 2 }, - CrewMemberSlotItem: { name: "CrewMemberBin", slotsPerPurchase: 1 } + SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 }, + TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 }, + TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 }, + SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 }, + TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 }, + MechSlotItem: { name: "MechBin", purchaseQuantity: 1 }, + TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 }, + RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 }, + TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 }, + CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 } }; // // extra = everything above the base +2 slots (depending on slot type) @@ -286,7 +286,8 @@ export const slotPurchaseNameToSlotName: SlotPurchase = { const handleSlotPurchase = ( slotPurchaseNameFull: string, inventory: TInventoryDatabaseDocument, - quantity: number + quantity: number, + ignorePurchaseQuantity: boolean ): IPurchaseResponse => { logger.debug(`slot name ${slotPurchaseNameFull}`); const slotPurchaseName = parseSlotPurchaseName( @@ -295,7 +296,10 @@ const handleSlotPurchase = ( logger.debug(`slot purchase name ${slotPurchaseName}`); const slotName = slotPurchaseNameToSlotName[slotPurchaseName].name; - const slotsPurchased = slotPurchaseNameToSlotName[slotPurchaseName].slotsPerPurchase * quantity; + let slotsPurchased = quantity; + if (!ignorePurchaseQuantity) { + slotsPurchased *= slotPurchaseNameToSlotName[slotPurchaseName].purchaseQuantity; + } updateSlots(inventory, slotName, slotsPurchased, slotsPurchased); @@ -360,7 +364,8 @@ const handleCreditBundlePurchase = async ( const handleTypesPurchase = async ( typesName: string, inventory: TInventoryDatabaseDocument, - quantity: number + quantity: number, + ignorePurchaseQuantity: boolean ): Promise => { const typeCategory = getStoreItemTypesCategory(typesName); logger.debug(`type category ${typeCategory}`); @@ -370,7 +375,7 @@ const handleTypesPurchase = async ( case "BoosterPacks": return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": - return handleSlotPurchase(typesName, inventory, quantity); + return handleSlotPurchase(typesName, inventory, quantity, ignorePurchaseQuantity); case "CreditBundles": return handleCreditBundlePurchase(typesName, inventory); } diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index a280787f..d14f39f5 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -105,5 +105,5 @@ export const slotNames = [ export type SlotNames = (typeof slotNames)[number]; export type SlotPurchase = { - [P in SlotPurchaseName]: { name: SlotNames; slotsPerPurchase: number }; + [P in SlotPurchaseName]: { name: SlotNames; purchaseQuantity: number }; }; From 7acb54922f37900680960192aeea2eb9786dc368 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 11 Mar 2025 13:00:12 -0700 Subject: [PATCH 145/776] fix: occupy a sentinel slot for sentinel weapons (#1156) Fixes #1155 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1156 --- src/services/inventoryService.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 59da757a..56a420c1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -518,15 +518,7 @@ export const addItem = async ( switch (typeName.substr(1).split("/")[2]) { case "Sentinels": { return { - InventoryChanges: { - ...addSentinel( - inventory, - typeName, - {}, - premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - ), - ...occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase) - } + InventoryChanges: addSentinel(inventory, typeName, premiumPurchase) }; } case "Game": { @@ -622,20 +614,24 @@ export const applyDefaultUpgrades = ( }; //TODO: maybe genericMethod for all the add methods, they share a lot of logic -export const addSentinel = ( +const addSentinel = ( inventory: TInventoryDatabaseDocument, sentinelName: string, - inventoryChanges: IInventoryChanges = {}, - features: number | undefined = undefined + premiumPurchase: boolean, + inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { + // Sentinel itself occupies a slot in the sentinels bin + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportSentinels[sentinelName]?.defaultWeapon) { - addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges); + addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, premiumPurchase, inventoryChanges); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades); + const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined; const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features }) - 1; inventoryChanges.Sentinels ??= []; @@ -644,11 +640,15 @@ export const addSentinel = ( return inventoryChanges; }; -export const addSentinelWeapon = ( +const addSentinelWeapon = ( inventory: TInventoryDatabaseDocument, typeName: string, + premiumPurchase: boolean, inventoryChanges: IInventoryChanges ): void => { + // Sentinel weapons also occupy a slot in the sentinels bin + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + const index = inventory.SentinelWeapons.push({ ItemType: typeName, XP: 0 }) - 1; inventoryChanges.SentinelWeapons ??= []; inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON()); From be6e5ce250f5478e8a93f6de010cfa43d60e3251 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 12 Mar 2025 01:08:15 -0700 Subject: [PATCH 146/776] feat: track ClassChanges in clan log (#1157) Re #1152 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1157 --- .../api/contributeGuildClassController.ts | 6 ++++ src/controllers/api/getGuildLogController.ts | 32 +++++++++++++++++-- src/models/guildModel.ts | 15 +++++++-- src/types/guildTypes.ts | 8 +++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index d0c91539..c5e878f7 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -20,6 +20,12 @@ export const contributeGuildClassController: RequestHandler = async (req, res) = for (let i = guild.Class; i != guild.CeremonyClass; ++i) { guild.CeremonyEndo += (i + 1) * 1000; } + guild.ClassChanges ??= []; + guild.ClassChanges.push({ + dateTime: new Date(), + entryType: 13, + details: guild.CeremonyClass + }); } guild.CeremonyContributors.push(new Types.ObjectId(accountId)); diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 2919ce31..ab19f5e1 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -1,11 +1,37 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IMongoDate } from "@/src/types/commonTypes"; import { RequestHandler } from "express"; -export const getGuildLogController: RequestHandler = (_req, res) => { - res.json({ +export const getGuildLogController: RequestHandler = async (req, res) => { + const log: Record = { RoomChanges: [], TechChanges: [], RosterActivity: [], StandingsUpdates: [], ClassChanges: [] - }); + }; + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + if (inventory.GuildId) { + const guild = await Guild.findOne({ _id: inventory.GuildId }); + if (guild) { + guild.ClassChanges?.forEach(entry => { + log.ClassChanges.push({ + dateTime: toMongoDate(entry.dateTime), + entryType: entry.entryType, + details: entry.details + }); + }); + } + } + res.json(log); }; + +interface IGuildLogEntryClient { + dateTime: IMongoDate; + entryType: number; + details: number | string; +} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 9815d257..f1d16068 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -5,7 +5,8 @@ import { ITechProjectClient, IDojoDecoDatabase, ILongMOTD, - IGuildMemberDatabase + IGuildMemberDatabase, + IGuildLogClassChange } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -69,6 +70,15 @@ const longMOTDSchema = new Schema( { _id: false } ); +const guildLogClassChangeSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: Number + }, + { _id: false } +); + const guildSchema = new Schema( { Name: { type: String, required: true, unique: true }, @@ -89,7 +99,8 @@ const guildSchema = new Schema( CeremonyClass: Number, CeremonyContributors: { type: [Types.ObjectId], default: undefined }, CeremonyResetDate: Date, - CeremonyEndo: Number + CeremonyEndo: Number, + ClassChanges: { type: [guildLogClassChangeSchema], default: undefined } }, { id: false } ); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 7f241212..01348693 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -46,6 +46,8 @@ export interface IGuildDatabase { CeremonyEndo?: number; CeremonyContributors?: Types.ObjectId[]; CeremonyResetDate?: Date; + + ClassChanges?: IGuildLogClassChange[]; } export interface ILongMOTD { @@ -160,3 +162,9 @@ export interface ITechProjectClient { export interface ITechProjectDatabase extends Omit { CompletionDate?: Date; } + +export interface IGuildLogClassChange { + dateTime: Date; + entryType: number; + details: number; +} From 8daf0c9eda92ed72265d674b9a7bd147ca0dd0cb Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 12 Mar 2025 12:41:45 +0100 Subject: [PATCH 147/776] fix(webui): add mods regression --- static/webui/script.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index 012388ea..34d65389 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -172,6 +172,16 @@ function fetchItemList() { window.archonCrystalUpgrades = data.archonCrystalUpgrades; + // Add mods mising in data sources + data.mods.push({ + uniqueName: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser", + name: loc("code_legendaryCore") + }); + data.mods.push({ + uniqueName: "/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod", + name: loc("code_traumaticPeculiar") + }); + const itemMap = { // Generics for rivens "/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": { name: loc("code_archgun") }, @@ -196,10 +206,7 @@ function fetchItemList() { "/Lotus/Weapons/Operator/Pistols/DrifterPistol/DrifterPistolPlayerWeapon": { name: loc("code_sirocco") }, - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kdrive") }, - // Missing in data sources - "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser": { name: loc("code_legendaryCore") }, - "/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod": { name: loc("code_traumaticPeculiar") } + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kdrive") } }; for (const [type, items] of Object.entries(data)) { if (type == "archonCrystalUpgrades") { From 02ce0f57a6b1954f38a4831da591a51b173ebc46 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 12 Mar 2025 05:10:26 -0700 Subject: [PATCH 148/776] chore: faithful response to getGuild & getGuildLog when not in a clan (#1159) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1159 --- src/controllers/api/getGuildController.ts | 2 +- src/controllers/api/getGuildLogController.ts | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 9dcfcbf1..0b7c5a95 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -28,7 +28,7 @@ const getGuildController: RequestHandler = async (req, res) => { return; } } - res.json({}); + res.sendStatus(200); }; export { getGuildController }; diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index ab19f5e1..245d857e 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -6,18 +6,18 @@ import { IMongoDate } from "@/src/types/commonTypes"; import { RequestHandler } from "express"; export const getGuildLogController: RequestHandler = async (req, res) => { - const log: Record = { - RoomChanges: [], - TechChanges: [], - RosterActivity: [], - StandingsUpdates: [], - ClassChanges: [] - }; const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); if (inventory.GuildId) { const guild = await Guild.findOne({ _id: inventory.GuildId }); if (guild) { + const log: Record = { + RoomChanges: [], + TechChanges: [], + RosterActivity: [], + StandingsUpdates: [], + ClassChanges: [] + }; guild.ClassChanges?.forEach(entry => { log.ClassChanges.push({ dateTime: toMongoDate(entry.dateTime), @@ -25,9 +25,11 @@ export const getGuildLogController: RequestHandler = async (req, res) => { details: entry.details }); }); + res.json(log); + return; } } - res.json(log); + res.sendStatus(200); }; interface IGuildLogEntryClient { From 42799fee7bbb57d57de50bd2291d59142f7e5d03 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 12 Mar 2025 06:31:14 -0700 Subject: [PATCH 149/776] fix(webui): Chinese translation of Traumatic Peculiar mod name (#1161) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1161 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/zh.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 78c167ba..2858309e 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -18,7 +18,7 @@ dict = { code_sirocco: `赤风`, code_kDrive: `K式悬浮板`, code_legendaryCore: `传奇核心`, - code_traumaticPeculiar: `Traumatic Peculiar`, + code_traumaticPeculiar: `创伤怪奇`, code_starter: `|MOD| (有瑕疵的)`, code_badItem: `(Imposter)`, code_maxRank: `满级`, From 073eddc050674ae71b2d36c7b7bdab830bf75803 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 12 Mar 2025 07:59:20 -0700 Subject: [PATCH 150/776] feat: track TechChanges in clan log (#1160) Re #1152 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1160 --- src/controllers/api/getGuildLogController.ts | 7 +++ src/controllers/api/guildTechController.ts | 57 ++++++++++++++++++-- src/models/guildModel.ts | 26 +++++---- src/types/guildTypes.ts | 7 +++ 4 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 245d857e..4d859a65 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -18,6 +18,13 @@ export const getGuildLogController: RequestHandler = async (req, res) => { StandingsUpdates: [], ClassChanges: [] }; + guild.TechChanges?.forEach(entry => { + log.TechChanges.push({ + dateTime: toMongoDate(entry.dateTime), + entryType: entry.entryType, + details: entry.details + }); + }); guild.ClassChanges?.forEach(entry => { log.ClassChanges.push({ dateTime: toMongoDate(entry.dateTime), diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 90714492..ff92244f 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -13,8 +13,9 @@ import { import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; -import { ITechProjectDatabase } from "@/src/types/guildTypes"; +import { ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; import { TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -23,9 +24,29 @@ export const guildTechController: RequestHandler = async (req, res) => { const data = JSON.parse(String(req.body)) as TGuildTechRequest; const action = data.Action.split(",")[0]; if (action == "Sync") { - res.json({ - TechProjects: guild.toJSON().TechProjects - }); + let needSave = false; + const techProjects: ITechProjectClient[] = []; + if (guild.TechProjects) { + for (const project of guild.TechProjects) { + const techProject: ITechProjectClient = { + ItemType: project.ItemType, + ReqCredits: project.ReqCredits, + ReqItems: project.ReqItems, + State: project.State + }; + if (project.CompletionDate) { + techProject.CompletionDate = toMongoDate(project.CompletionDate); + if (Date.now() >= project.CompletionDate.getTime()) { + needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate); + } + } + techProjects.push(techProject); + } + } + if (needSave) { + await guild.save(); + } + res.json({ TechProjects: techProjects }); } else if (action == "Start") { const recipe = ExportDojoRecipes.research[data.RecipeType!]; guild.TechProjects ??= []; @@ -42,6 +63,7 @@ export const guildTechController: RequestHandler = async (req, res) => { State: 0 }) - 1 ]; + setTechLogState(guild, techProject.ItemType, 5); if (config.noDojoResearchCosts) { processFundedProject(guild, techProject, recipe); } @@ -159,10 +181,35 @@ const processFundedProject = ( recipe: IDojoResearch ): void => { techProject.State = 1; - techProject.CompletionDate = new Date(new Date().getTime() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); + techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); if (recipe.guildXpValue) { guild.XP += recipe.guildXpValue; } + setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); +}; + +const setTechLogState = ( + guild: TGuildDatabaseDocument, + type: string, + state: number, + dateTime: Date | undefined = undefined +): boolean => { + guild.TechChanges ??= []; + const entry = guild.TechChanges.find(x => x.details == type); + if (entry) { + if (entry.entryType == state) { + return false; + } + entry.dateTime = dateTime ?? new Date(); + entry.entryType = state; + } else { + guild.TechChanges.push({ + dateTime: dateTime ?? new Date(), + entryType: state, + details: type + }); + } + return true; }; type TGuildTechRequest = diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index f1d16068..3c786ca8 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -2,15 +2,14 @@ import { IGuildDatabase, IDojoComponentDatabase, ITechProjectDatabase, - ITechProjectClient, IDojoDecoDatabase, ILongMOTD, IGuildMemberDatabase, - IGuildLogClassChange + IGuildLogClassChange, + IGuildLogTechChange } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; -import { toMongoDate } from "../helpers/inventoryHelpers"; const dojoDecoSchema = new Schema({ Type: String, @@ -51,17 +50,6 @@ const techProjectSchema = new Schema( { _id: false } ); -techProjectSchema.set("toJSON", { - virtuals: true, - transform(_doc, obj) { - const db = obj as ITechProjectDatabase; - const client = obj as ITechProjectClient; - if (db.CompletionDate) { - client.CompletionDate = toMongoDate(db.CompletionDate); - } - } -}); - const longMOTDSchema = new Schema( { message: String, @@ -70,6 +58,15 @@ const longMOTDSchema = new Schema( { _id: false } ); +const guildLogTechChangeSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: String + }, + { _id: false } +); + const guildLogClassChangeSchema = new Schema( { dateTime: Date, @@ -100,6 +97,7 @@ const guildSchema = new Schema( CeremonyContributors: { type: [Types.ObjectId], default: undefined }, CeremonyResetDate: Date, CeremonyEndo: Number, + TechChanges: { type: [guildLogTechChangeSchema], default: undefined }, ClassChanges: { type: [guildLogClassChangeSchema], default: undefined } }, { id: false } diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 01348693..de04588d 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -47,6 +47,7 @@ export interface IGuildDatabase { CeremonyContributors?: Types.ObjectId[]; CeremonyResetDate?: Date; + TechChanges?: IGuildLogTechChange[]; ClassChanges?: IGuildLogClassChange[]; } @@ -163,6 +164,12 @@ export interface ITechProjectDatabase extends Omit Date: Wed, 12 Mar 2025 07:59:29 -0700 Subject: [PATCH 151/776] feat: promote & demote clan members (#1163) Re #1144 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1163 --- .../api/changeGuildRankController.ts | 28 +++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 src/controllers/api/changeGuildRankController.ts diff --git a/src/controllers/api/changeGuildRankController.ts b/src/controllers/api/changeGuildRankController.ts new file mode 100644 index 00000000..3293d251 --- /dev/null +++ b/src/controllers/api/changeGuildRankController.ts @@ -0,0 +1,28 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { RequestHandler } from "express"; + +export const changeGuildRankController: RequestHandler = async (req, res) => { + // TODO: Verify permissions + const guildMember = (await GuildMember.findOne({ + guildId: req.query.guildId as string, + accountId: req.query.targetId as string + }))!; + guildMember.rank = parseInt(req.query.rankChange as string); + await guildMember.save(); + + if (guildMember.rank == 0) { + // If we just promoted someone else to Founding Warlord, we need to demote ourselves to Warlord. + await GuildMember.findOneAndUpdate( + { + guildId: req.query.guildId as string, + accountId: req.query.accountId as string + }, + { rank: 1 } + ); + } + + res.json({ + _id: req.query.targetId as string, + Rank: guildMember.rank + }); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 75ddf345..c2cf107f 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -10,6 +10,7 @@ import { archonFusionController } from "@/src/controllers/api/archonFusionContro import { artifactsController } from "@/src/controllers/api/artifactsController"; import { artifactTransmutationController } from "@/src/controllers/api/artifactTransmutationController"; import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; +import { changeGuildRankController } from "@/src/controllers/api/changeGuildRankController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; @@ -115,6 +116,7 @@ const apiRouter = express.Router(); // get apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); +apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController); From 2ad95aecb6a386e4f695ee8fa21d22ba6c740c17 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 12 Mar 2025 07:59:35 -0700 Subject: [PATCH 152/776] chore: npm update (#1162) The package-lock.json is smaller now, that seems good. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1162 --- package-lock.json | 287 ++++++++++++++++++---------------------------- 1 file changed, 111 insertions(+), 176 deletions(-) diff --git a/package-lock.json b/package-lock.json index 486ac1a0..a9e619d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,9 +70,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", + "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", "dev": true, "license": "MIT", "dependencies": { @@ -250,6 +250,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } @@ -365,9 +366,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", - "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -398,9 +399,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.9.tgz", - "integrity": "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -462,12 +463,14 @@ "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", "dependencies": { "@types/webidl-conversions": "*" } @@ -666,9 +669,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", - "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, @@ -686,9 +689,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -790,12 +793,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-flatten": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", - "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", - "license": "MIT" - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -850,82 +847,38 @@ } }, "node_modules/body-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", - "integrity": "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", + "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "3.1.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.5.2", - "on-finished": "2.4.1", - "qs": "6.13.0", + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.5.2", + "on-finished": "^2.4.1", + "qs": "^6.14.0", "raw-body": "^3.0.0", - "type-is": "~1.6.18" + "type-is": "^2.0.0" }, "engines": { "node": ">=18" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "license": "MIT", + "node_modules/body-parser/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" + "side-channel": "^1.1.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "node": ">=0.6" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/brace-expansion": { @@ -955,6 +908,7 @@ "version": "6.10.3", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "license": "Apache-2.0", "engines": { "node": ">=16.20.1" } @@ -976,9 +930,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -989,13 +943,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1239,6 +1193,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" }, @@ -1796,9 +1751,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -1847,47 +1802,22 @@ } }, "node_modules/finalhandler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", - "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1921,9 +1851,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -1991,17 +1921,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -2212,9 +2142,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2536,7 +2466,8 @@ "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" }, "node_modules/merge-descriptors": { "version": "2.0.0", @@ -2652,9 +2583,10 @@ } }, "node_modules/mongodb": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.1.tgz", - "integrity": "sha512-gdq40tX8StmhP6akMp1pPoEVv+9jTYFSrga/g23JxajPAQhH39ysZrHGzQCSd9PEOnuEQEdjIWqxO7ZSwC0w7Q==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", + "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", + "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.3", @@ -2664,7 +2596,7 @@ "node": ">=16.20.1" }, "peerDependencies": { - "@aws-sdk/credential-providers": "^3.632.0", + "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", @@ -2700,19 +2632,21 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.11.0.tgz", - "integrity": "sha512-xaQSuaLk2JKmXI5zDVVWXVCQTnWhAe8MFOijMnwOuP/wucKVphd3f+ouDKivCDMGjYBDrR7dtoyV0U093xbKqA==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", + "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", + "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.13.0", + "mongodb": "~6.14.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -2842,9 +2776,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3043,9 +2977,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -3237,9 +3171,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -3265,21 +3199,17 @@ } }, "node_modules/router": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", - "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", + "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", "license": "MIT", "dependencies": { - "array-flatten": "3.0.0", - "is-promise": "4.0.0", - "methods": "~1.1.2", - "parseurl": "~1.3.3", - "path-to-regexp": "^8.0.0", - "setprototypeof": "1.2.0", - "utils-merge": "1.0.1" + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 18" } }, "node_modules/run-parallel": { @@ -3342,9 +3272,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -3573,6 +3503,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" } @@ -3778,6 +3709,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, @@ -3999,6 +3931,7 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4086,6 +4019,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } @@ -4094,6 +4028,7 @@ "version": "14.1.1", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "license": "MIT", "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" From 516df61633481342273af4cdd6315a89f92cee11 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 02:14:21 -0700 Subject: [PATCH 153/776] chore: add "project status" section to readme (#1166) Explicitly pointing out the issue tracking should give people a better idea of the project and allow them to set expectations accordingly. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1166 --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f68f700..55edddd0 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,11 @@ More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY) ->[!NOTE] ->Development of this project currently happens on . If that's not the site you're on, you're looking at a one-way mirror. +## Project Status + +This project is in active development at . + +To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors. ## config.json From b7800b6d2012874ca55bfe167f77d3d6c919b99b Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 02:14:29 -0700 Subject: [PATCH 154/776] feat: edit clan hierarchy (#1164) Re #1144 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1164 --- .../api/customizeGuildRanksController.ts | 16 ++++++ src/models/guildModel.ts | 51 ++++++++++++++++++- src/routes/api.ts | 2 + src/services/guildService.ts | 39 +------------- src/types/guildTypes.ts | 22 ++++++++ 5 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 src/controllers/api/customizeGuildRanksController.ts diff --git a/src/controllers/api/customizeGuildRanksController.ts b/src/controllers/api/customizeGuildRanksController.ts new file mode 100644 index 00000000..3e237a81 --- /dev/null +++ b/src/controllers/api/customizeGuildRanksController.ts @@ -0,0 +1,16 @@ +import { getGuildForRequest } from "@/src/services/guildService"; +import { IGuildRank } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const customizeGuildRanksController: RequestHandler = async (req, res) => { + const guild = await getGuildForRequest(req); + const payload = JSON.parse(String(req.body)) as ICustomizeGuildRanksRequest; + // TODO: Verify permissions + guild.Ranks = payload.GuildRanks; + await guild.save(); + res.end(); +}; + +interface ICustomizeGuildRanksRequest { + GuildRanks: IGuildRank[]; +} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 3c786ca8..ec2a8159 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -6,7 +6,8 @@ import { ILongMOTD, IGuildMemberDatabase, IGuildLogClassChange, - IGuildLogTechChange + IGuildLogTechChange, + IGuildRank } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -58,6 +59,53 @@ const longMOTDSchema = new Schema( { _id: false } ); +const guildRankSchema = new Schema( + { + Name: String, + Permissions: Number + }, + { _id: false } +); + +const defaultRanks: IGuildRank[] = [ + { + Name: "/Lotus/Language/Game/Rank_Creator", + Permissions: 16351 + }, + { + Name: "/Lotus/Language/Game/Rank_Warlord", + Permissions: 16351 + }, + { + Name: "/Lotus/Language/Game/Rank_General", + Permissions: 4318 + }, + { + Name: "/Lotus/Language/Game/Rank_Officer", + Permissions: 4314 + }, + { + Name: "/Lotus/Language/Game/Rank_Leader", + Permissions: 4106 + }, + { + Name: "/Lotus/Language/Game/Rank_Sage", + Permissions: 4304 + }, + { + Name: "/Lotus/Language/Game/Rank_Soldier", + Permissions: 4098 + }, + { + Name: "/Lotus/Language/Game/Rank_Initiate", + Permissions: 4096 + }, + { + Name: "/Lotus/Language/Game/Rank_Utility", + Permissions: 4096 + } +]; + const guildLogTechChangeSchema = new Schema( { dateTime: Date, @@ -81,6 +129,7 @@ const guildSchema = new Schema( Name: { type: String, required: true, unique: true }, MOTD: { type: String, default: "" }, LongMOTD: { type: longMOTDSchema, default: undefined }, + Ranks: { type: [guildRankSchema], default: defaultRanks }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, diff --git a/src/routes/api.ts b/src/routes/api.ts index c2cf107f..b2e27b53 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -22,6 +22,7 @@ import { contributeToDojoComponentController } from "@/src/controllers/api/contr import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; @@ -171,6 +172,7 @@ apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); apiRouter.post("/drones.php", dronesController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 3d53a211..061e685f 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -86,44 +86,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s MOTD: guild.MOTD, LongMOTD: guild.LongMOTD, Members: members, - Ranks: [ - { - Name: "/Lotus/Language/Game/Rank_Creator", - Permissions: 16351 - }, - { - Name: "/Lotus/Language/Game/Rank_Warlord", - Permissions: 14303 - }, - { - Name: "/Lotus/Language/Game/Rank_General", - Permissions: 4318 - }, - { - Name: "/Lotus/Language/Game/Rank_Officer", - Permissions: 4314 - }, - { - Name: "/Lotus/Language/Game/Rank_Leader", - Permissions: 4106 - }, - { - Name: "/Lotus/Language/Game/Rank_Sage", - Permissions: 4304 - }, - { - Name: "/Lotus/Language/Game/Rank_Soldier", - Permissions: 4098 - }, - { - Name: "/Lotus/Language/Game/Rank_Initiate", - Permissions: 4096 - }, - { - Name: "/Lotus/Language/Game/Rank_Utility", - Permissions: 4096 - } - ], + Ranks: guild.Ranks, Tier: 1, Vault: getGuildVault(guild), Class: guild.Class, diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index de04588d..df7f7031 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -26,6 +26,7 @@ export interface IGuildDatabase { Name: string; MOTD: string; LongMOTD?: ILongMOTD; + Ranks: IGuildRank[]; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; @@ -57,6 +58,27 @@ export interface ILongMOTD { //authorGuildName: ""; } +// 32 seems to be reserved +export enum GuildPermission { + Ruler = 1, // Change clan hierarchy + Advertiser = 8192, + Recruiter = 2, // Invite members + Regulator = 4, // Kick members + Promoter = 8, // Promote and demote members + Architect = 16, // Create and destroy rooms + Decorator = 1024, // Create and destroy decos + Treasurer = 64, // Contribute from vault and edit tax rate + Tech = 128, // Queue research + ChatModerator = 512, + Herald = 2048, // Change MOTD + Fabricator = 4096 // Replicate research +} + +export interface IGuildRank { + Name: string; + Permissions: number; +} + export interface IGuildMemberDatabase { accountId: Types.ObjectId; guildId: Types.ObjectId; From 6490fadcaed4038985f450de490d6294ecfbc502 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 02:14:53 -0700 Subject: [PATCH 155/776] feat: track vendor purchases (#1153) Closes #739 Also adds the `noVendorPurchaseLimits` cheat to disable the logic, which is enabled by default due to lack of vendor rotations. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1153 --- config.json.example | 1 + src/models/inventoryModels/inventoryModel.ts | 32 +++++++++++++++- src/services/configService.ts | 1 + src/services/purchaseService.ts | 39 ++++++++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 26 ++++++++++++- static/webui/index.html | 4 ++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + 10 files changed, 104 insertions(+), 3 deletions(-) diff --git a/config.json.example b/config.json.example index d12270ef..f1c29035 100644 --- a/config.json.example +++ b/config.json.example @@ -29,6 +29,7 @@ "unlockExilusEverywhere": false, "unlockArcanesEverywhere": false, "noDailyStandingLimits": false, + "noVendorPurchaseLimits": true, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": false, "fastDojoRoomDestruction": false, diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 880dd09e..c7446f41 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -76,7 +76,10 @@ import { IIncentiveState, ISongChallenge, ILibraryPersonalProgress, - ICrewShipWeaponDatabase + ICrewShipWeaponDatabase, + IRecentVendorPurchaseDatabase, + IVendorPurchaseHistoryEntryDatabase, + IVendorPurchaseHistoryEntryClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -974,6 +977,31 @@ const incentiveStateSchema = new Schema( { _id: false } ); +const vendorPurchaseHistoryEntrySchema = new Schema( + { + Expiry: Date, + NumPurchased: Number, + ItemId: String + }, + { _id: false } +); + +vendorPurchaseHistoryEntrySchema.set("toJSON", { + transform(_doc, obj) { + const db = obj as IVendorPurchaseHistoryEntryDatabase; + const client = obj as IVendorPurchaseHistoryEntryClient; + client.Expiry = toMongoDate(db.Expiry); + } +}); + +const recentVendorPurchaseSchema = new Schema( + { + VendorType: String, + PurchaseHistory: [vendorPurchaseHistoryEntrySchema] + }, + { _id: false } +); + const collectibleEntrySchema = new Schema( { CollectibleType: String, @@ -1361,7 +1389,7 @@ const inventorySchema = new Schema( RandomUpgradesIdentified: Number, BountyScore: Number, ChallengeInstanceStates: [Schema.Types.Mixed], - RecentVendorPurchases: [Schema.Types.Mixed], + RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined }, Robotics: [Schema.Types.Mixed], UsedDailyDeals: [Schema.Types.Mixed], CollectibleSeries: { type: [collectibleEntrySchema], default: undefined }, diff --git a/src/services/configService.ts b/src/services/configService.ts index 07860f93..717dae35 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -55,6 +55,7 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noVendorPurchaseLimits?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; fastDojoRoomDestruction?: boolean; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 19e683fe..d960c629 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -68,6 +68,45 @@ export const handlePurchase = async ( inventoryChanges ); } + if (!config.noVendorPurchaseLimits) { + inventory.RecentVendorPurchases ??= []; + let vendorPurchases = inventory.RecentVendorPurchases.find( + x => x.VendorType == manifest.VendorInfo.TypeName + ); + if (!vendorPurchases) { + vendorPurchases = + inventory.RecentVendorPurchases[ + inventory.RecentVendorPurchases.push({ + VendorType: manifest.VendorInfo.TypeName, + PurchaseHistory: [] + }) - 1 + ]; + } + const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); + let numPurchased = purchaseRequest.PurchaseParams.Quantity; + if (historyEntry) { + numPurchased += historyEntry.NumPurchased; + historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; + } else { + vendorPurchases.PurchaseHistory.push({ + ItemId: ItemId, + NumPurchased: purchaseRequest.PurchaseParams.Quantity, + Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) + }); + } + inventoryChanges.RecentVendorPurchases = [ + { + VendorType: manifest.VendorInfo.TypeName, + PurchaseHistory: [ + { + ItemId: ItemId, + NumPurchased: numPurchased, + Expiry: offer.Expiry + } + ] + } + ]; + } purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; } else if (!ExportVendors[purchaseRequest.PurchaseParams.SourceId!]) { throw new Error(`unknown vendor: ${purchaseRequest.PurchaseParams.SourceId!}`); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index af029a34..1e46376c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -41,6 +41,7 @@ export interface IInventoryDatabase | "KubrowPetEggs" | "PendingCoupon" | "Drones" + | "RecentVendorPurchases" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -67,6 +68,7 @@ export interface IInventoryDatabase KubrowPetEggs?: IKubrowPetEggDatabase[]; PendingCoupon?: IPendingCouponDatabase; Drones: IDroneDatabase[]; + RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; } export interface IQuestKeyDatabase { @@ -277,7 +279,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu BountyScore: number; ChallengeInstanceStates: IChallengeInstanceState[]; LoginMilestoneRewards: string[]; - RecentVendorPurchases: Array; + RecentVendorPurchases?: IRecentVendorPurchaseClient[]; NodeIntrosCompleted: string[]; GuildId?: IOid; CompletedJobChains: ICompletedJobChain[]; @@ -361,6 +363,28 @@ export interface IParam { v: string; } +export interface IRecentVendorPurchaseClient { + VendorType: string; + PurchaseHistory: IVendorPurchaseHistoryEntryClient[]; +} + +export interface IVendorPurchaseHistoryEntryClient { + Expiry: IMongoDate; + NumPurchased: number; + ItemId: string; +} + +export interface IRecentVendorPurchaseDatabase { + VendorType: string; + PurchaseHistory: IVendorPurchaseHistoryEntryDatabase[]; +} + +export interface IVendorPurchaseHistoryEntryDatabase { + Expiry: Date; + NumPurchased: number; + ItemId: string; +} + export interface IChallengeProgress { Progress: number; Name: string; diff --git a/static/webui/index.html b/static/webui/index.html index f700994d..bd5f13cc 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -517,6 +517,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 74ec43f4..0a329181 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, + cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 484b0063..455ab2e4 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -109,6 +109,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 153aad2b..9840911a 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, + cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 44c6eb41..030c704b 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, From a029c288b78317fac44483b12f851b90f56ff4cc Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 04:25:59 -0700 Subject: [PATCH 156/776] fix: free slot when selling or otherwise getting rid of items (#1169) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1169 --- .../api/claimCompletedRecipeController.ts | 12 +++++++++-- .../api/infestedFoundryController.ts | 4 +++- src/controllers/api/sellController.ts | 21 ++++++++++++++++++- src/controllers/api/startRecipeController.ts | 5 +++-- src/services/inventoryService.ts | 7 ++++++- src/types/inventoryTypes/inventoryTypes.ts | 2 ++ 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 325d10ff..5b4f1acb 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -7,10 +7,17 @@ import { getRecipe } from "@/src/services/itemDataService"; import { IOid } from "@/src/types/commonTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; +import { + getInventory, + updateCurrency, + addItem, + addMiscItems, + addRecipes, + occupySlot +} from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -53,6 +60,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = inventoryChanges[category].push(inventory[category][index].toJSON()); nonMiscItemIngredients.add(item.ItemType); + occupySlot(inventory, InventorySlot.WEAPONS, false); inventoryChanges.WeaponBin ??= { Slots: 0 }; inventoryChanges.WeaponBin.Slots -= 1; }); diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index 1a0ac5f7..aa7c99f5 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -1,7 +1,7 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory, addMiscItems, updateCurrency, addRecipes } from "@/src/services/inventoryService"; +import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService"; import { IOid } from "@/src/types/commonTypes"; import { IConsumedSuit, @@ -10,6 +10,7 @@ import { IInfestedFoundryDatabase, IInventoryClient, IMiscItem, + InventorySlot, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { ExportMisc, ExportRecipes } from "warframe-public-export-plus"; @@ -264,6 +265,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { ); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00); addRecipes(inventory, recipeChanges); + freeUpSlot(inventory, InventorySlot.SUITS); await inventory.save(); const infestedFoundry = inventory.toJSON().InfestedFoundry!; applyCheatsToInfestedFoundry(infestedFoundry); diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index 466bd197..fa8f3f01 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -1,6 +1,14 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, addMods, addRecipes, addMiscItems, addConsumables } from "@/src/services/inventoryService"; +import { + getInventory, + addMods, + addRecipes, + addMiscItems, + addConsumables, + freeUpSlot +} from "@/src/services/inventoryService"; +import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export const sellController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as ISellRequest; @@ -34,56 +42,67 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.Suits) { payload.Items.Suits.forEach(sellItem => { inventory.Suits.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SUITS); }); } if (payload.Items.LongGuns) { payload.Items.LongGuns.forEach(sellItem => { inventory.LongGuns.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.WEAPONS); }); } if (payload.Items.Pistols) { payload.Items.Pistols.forEach(sellItem => { inventory.Pistols.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.WEAPONS); }); } if (payload.Items.Melee) { payload.Items.Melee.forEach(sellItem => { inventory.Melee.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.WEAPONS); }); } if (payload.Items.SpaceSuits) { payload.Items.SpaceSuits.forEach(sellItem => { inventory.SpaceSuits.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACESUITS); }); } if (payload.Items.SpaceGuns) { payload.Items.SpaceGuns.forEach(sellItem => { inventory.SpaceGuns.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); }); } if (payload.Items.SpaceMelee) { payload.Items.SpaceMelee.forEach(sellItem => { inventory.SpaceMelee.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); }); } if (payload.Items.Sentinels) { payload.Items.Sentinels.forEach(sellItem => { inventory.Sentinels.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SENTINELS); }); } if (payload.Items.SentinelWeapons) { payload.Items.SentinelWeapons.forEach(sellItem => { inventory.SentinelWeapons.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SENTINELS); }); } if (payload.Items.OperatorAmps) { payload.Items.OperatorAmps.forEach(sellItem => { inventory.OperatorAmps.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.AMPS); }); } if (payload.Items.Hoverboards) { payload.Items.Hoverboards.forEach(sellItem => { inventory.Hoverboards.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACESUITS); }); } if (payload.Items.Drones) { diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index d2f37230..0b8c4667 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -3,10 +3,10 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { getRecipe } from "@/src/services/itemDataService"; -import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { Types } from "mongoose"; -import { ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; +import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { ExportWeapons } from "warframe-public-export-plus"; @@ -53,6 +53,7 @@ export const startRecipeController: RequestHandler = async (req, res) => { pr[category] ??= []; pr[category].push(inventory[category][equipmentIndex]); inventory[category].splice(equipmentIndex, 1); + freeUpSlot(inventory, InventorySlot.WEAPONS); } else { addMiscItems(inventory, [ { diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 56a420c1..42e0fa66 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -172,7 +172,7 @@ export const getInventory = async ( return inventory; }; -const occupySlot = ( +export const occupySlot = ( inventory: TInventoryDatabaseDocument, bin: InventorySlot, premiumPurchase: boolean @@ -193,6 +193,11 @@ const occupySlot = ( return inventoryChanges; }; +export const freeUpSlot = (inventory: TInventoryDatabaseDocument, bin: InventorySlot): void => { + // { count: -1, platinum: 0, Slots: 1 } + updateSlots(inventory, bin, 1, 0); +}; + export const addItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 1e46376c..6fff6735 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -452,9 +452,11 @@ export enum InventorySlot { SUITS = "SuitBin", WEAPONS = "WeaponBin", SPACESUITS = "SpaceSuitBin", + SPACEWEAPONS = "SpaceWeaponBin", MECHSUITS = "MechBin", PVE_LOADOUTS = "PveBonusLoadoutBin", SENTINELS = "SentinelBin", + AMPS = "OperatorAmpBin", RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin" } From 292ac9d41bd4570d05edf5d1fb42011226d06948 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 04:26:06 -0700 Subject: [PATCH 157/776] fix: deduct 5000 credits for crafting a zaw (#1168) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1168 --- src/controllers/api/modularWeaponCraftingController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 4be0d980..1f041f12 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -59,7 +59,9 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) } const currencyChanges = updateCurrency( inventory, - category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000, + category == "Hoverboards" || category == "MoaPets" || category == "LongGuns" || category == "Pistols" + ? 5000 + : 4000, // Definitely correct for Melee & OperatorAmps false ); addMiscItems(inventory, miscItemChanges); From de4fe0311cf724da147d9de9b2156d4161fcf7f5 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 05:25:46 -0700 Subject: [PATCH 158/776] feat: trade in modular weapons for standing (#1172) Closes #1055 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1172 --- package-lock.json | 8 +-- package.json | 2 +- .../api/syndicateStandingBonusController.ts | 50 ++++++++++++++++--- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9e619d9..a9c49d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=4.7.4 <5.6.0", - "warframe-public-export-plus": "^0.5.43", + "warframe-public-export-plus": "^0.5.44", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4006,9 +4006,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.43", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.43.tgz", - "integrity": "sha512-LeF7HmsjOPsJDtgr66x3iMEIAQgcxKNM54VG895FTemgHLLo34UGDyeS1yIfY67WxxbTUgW3MkHQLlCEJXD14w==" + "version": "0.5.44", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.44.tgz", + "integrity": "sha512-0EH3CQBCuuELiLBL1brc/o6Qx8CK723TJF5o68VXc60ha93Juo6LQ+dV+QgzFvVQ5RZTjBLtKB4MP8qw3YHCUQ==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index eb4ceaa7..16610896 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=4.7.4 <5.6.0", - "warframe-public-export-plus": "^0.5.43", + "warframe-public-export-plus": "^0.5.44", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/syndicateStandingBonusController.ts b/src/controllers/api/syndicateStandingBonusController.ts index 6899ee3e..4067e9db 100644 --- a/src/controllers/api/syndicateStandingBonusController.ts +++ b/src/controllers/api/syndicateStandingBonusController.ts @@ -1,10 +1,19 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { + addMiscItems, + freeUpSlot, + getInventory, + getStandingLimit, + updateStandingLimit +} from "@/src/services/inventoryService"; +import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IOid } from "@/src/types/commonTypes"; -import { ExportSyndicates } from "warframe-public-export-plus"; +import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; +import { logger } from "@/src/utils/logger"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; export const syndicateStandingBonusController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -12,6 +21,7 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) const syndicateMeta = ExportSyndicates[request.Operation.AffiliationTag]; + // Process items let gainedStanding = 0; request.Operation.Items.forEach(item => { const medallion = (syndicateMeta.medallions ?? []).find(medallion => medallion.itemType == item.ItemType); @@ -21,9 +31,35 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) item.ItemCount *= -1; }); - const inventory = await getInventory(accountId); addMiscItems(inventory, request.Operation.Items); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges.MiscItems = request.Operation.Items; + + // Process modular weapon + if (request.Operation.ModularWeaponId.$oid != "000000000000000000000000") { + const category = req.query.Category as "LongGuns" | "Pistols" | "Melee" | "OperatorAmps"; + const weapon = inventory[category].id(request.Operation.ModularWeaponId.$oid)!; + if (gainedStanding !== 0) { + throw new Error(`modular weapon standing bonus should be mutually exclusive`); + } + weapon.ModularParts!.forEach(part => { + const partStandingBonus = ExportWeapons[part].donationStandingBonus; + if (partStandingBonus === undefined) { + throw new Error(`no standing bonus for ${part}`); + } + logger.debug(`modular weapon part ${part} gives ${partStandingBonus} standing`); + gainedStanding += partStandingBonus; + }); + if (weapon.Features && (weapon.Features & EquipmentFeatures.GILDED) != 0) { + gainedStanding *= 2; + } + inventoryChanges.RemovedIdItems = [{ ItemId: request.Operation.ModularWeaponId }]; + inventory[category].pull({ _id: request.Operation.ModularWeaponId.$oid }); + const slotBin = category == "OperatorAmps" ? InventorySlot.AMPS : InventorySlot.WEAPONS; + freeUpSlot(inventory, slotBin); + inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; + } let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag); if (!syndicate) { @@ -50,9 +86,7 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) await inventory.save(); res.json({ - InventoryChanges: { - MiscItems: request.Operation.Items - }, + InventoryChanges: inventoryChanges, AffiliationMods: [ { Tag: request.Operation.AffiliationTag, @@ -67,6 +101,6 @@ interface ISyndicateStandingBonusRequest { AffiliationTag: string; AlternateBonusReward: ""; // ??? Items: IMiscItem[]; + ModularWeaponId: IOid; }; - ModularWeaponId: IOid; // Seems to just be "000000000000000000000000", also note there's a "Category" query field } From 3a995ef6d1084d1c65312884f1289e2394e56cb1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 13:26:24 +0100 Subject: [PATCH 159/776] chore: specify minimum typescript version required to compile --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9c49d28..c7a9d1bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "express": "^5", "mongoose": "^8.11.0", "morgan": "^1.10.0", - "typescript": ">=4.7.4 <5.6.0", + "typescript": ">=5.5 <5.6.0", "warframe-public-export-plus": "^0.5.44", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", diff --git a/package.json b/package.json index 16610896..3cb5e8cc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "express": "^5", "mongoose": "^8.11.0", "morgan": "^1.10.0", - "typescript": ">=4.7.4 <5.6.0", + "typescript": ">=5.5 <5.6.0", "warframe-public-export-plus": "^0.5.44", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", From 6508d16190fcba825f7072bd53196eb046cfe25c Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 13 Mar 2025 10:46:08 -0700 Subject: [PATCH 160/776] feat: track RosterActivity in clan log (#1173) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1173 --- .../api/confirmGuildInvitationController.ts | 24 +++++++++++++++---- src/controllers/api/getGuildLogController.ts | 7 ++++++ .../api/removeFromGuildController.ts | 22 +++++++++++++++++ src/models/guildModel.ts | 13 +++++----- src/types/guildTypes.ts | 9 +++---- 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts index 070c9c28..7331af03 100644 --- a/src/controllers/api/confirmGuildInvitationController.ts +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -1,22 +1,36 @@ import { Guild, GuildMember } from "@/src/models/guildModel"; import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { RequestHandler } from "express"; import { Types } from "mongoose"; export const confirmGuildInvitationController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); const guildMember = await GuildMember.findOne({ - accountId: accountId, + accountId: account._id, guildId: req.query.clanId as string }); if (guildMember) { guildMember.status = 0; await guildMember.save(); - await updateInventoryForConfirmedGuildJoin(accountId, new Types.ObjectId(req.query.clanId as string)); + + await updateInventoryForConfirmedGuildJoin( + account._id.toString(), + new Types.ObjectId(req.query.clanId as string) + ); + const guild = (await Guild.findOne({ _id: req.query.clanId as string }))!; + + guild.RosterActivity ??= []; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 6, + details: getSuffixedName(account) + }); + await guild.save(); + res.json({ - ...(await getGuildClient(guild, accountId)), + ...(await getGuildClient(guild, account._id.toString())), InventoryChanges: { Recipes: [ { diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 4d859a65..9d4b82c5 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -25,6 +25,13 @@ export const getGuildLogController: RequestHandler = async (req, res) => { details: entry.details }); }); + guild.RosterActivity?.forEach(entry => { + log.RosterActivity.push({ + dateTime: toMongoDate(entry.dateTime), + entryType: entry.entryType, + details: entry.details + }); + }); guild.ClassChanges?.forEach(entry => { log.ClassChanges.push({ dateTime: toMongoDate(entry.dateTime), diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index c28700e4..3b886099 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -1,9 +1,12 @@ import { GuildMember } from "@/src/models/guildModel"; +import { Account } from "@/src/models/loginModel"; import { getGuildForRequest } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { RequestHandler } from "express"; export const removeFromGuildController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); const guild = await getGuildForRequest(req); // TODO: Check permissions const payload = JSON.parse(String(req.body)) as IRemoveFromGuildRequest; @@ -32,6 +35,25 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { } await GuildMember.deleteOne({ _id: guildMember._id }); + guild.RosterActivity ??= []; + if (account._id.equals(payload.userId)) { + // Leave + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 7, + details: getSuffixedName(account) + }); + } else { + // Kick + const kickee = (await Account.findOne({ _id: payload.userId }))!; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 12, + details: getSuffixedName(kickee) + "," + getSuffixedName(account) + }); + } + await guild.save(); + res.json({ _id: payload.userId, ItemToRemove: "/Lotus/Types/Keys/DojoKey", diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index ec2a8159..195fe611 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -5,8 +5,8 @@ import { IDojoDecoDatabase, ILongMOTD, IGuildMemberDatabase, - IGuildLogClassChange, - IGuildLogTechChange, + IGuildLogEntryNumber, + IGuildLogEntryString, IGuildRank } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; @@ -106,7 +106,7 @@ const defaultRanks: IGuildRank[] = [ } ]; -const guildLogTechChangeSchema = new Schema( +const guildLogEntryStringSchema = new Schema( { dateTime: Date, entryType: Number, @@ -115,7 +115,7 @@ const guildLogTechChangeSchema = new Schema( { _id: false } ); -const guildLogClassChangeSchema = new Schema( +const guildLogEntryNumberSchema = new Schema( { dateTime: Date, entryType: Number, @@ -146,8 +146,9 @@ const guildSchema = new Schema( CeremonyContributors: { type: [Types.ObjectId], default: undefined }, CeremonyResetDate: Date, CeremonyEndo: Number, - TechChanges: { type: [guildLogTechChangeSchema], default: undefined }, - ClassChanges: { type: [guildLogClassChangeSchema], default: undefined } + TechChanges: { type: [guildLogEntryStringSchema], default: undefined }, + RosterActivity: { type: [guildLogEntryStringSchema], default: undefined }, + ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined } }, { id: false } ); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index df7f7031..e62cacc5 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -48,8 +48,9 @@ export interface IGuildDatabase { CeremonyContributors?: Types.ObjectId[]; CeremonyResetDate?: Date; - TechChanges?: IGuildLogTechChange[]; - ClassChanges?: IGuildLogClassChange[]; + TechChanges?: IGuildLogEntryString[]; + RosterActivity?: IGuildLogEntryString[]; + ClassChanges?: IGuildLogEntryNumber[]; } export interface ILongMOTD { @@ -186,13 +187,13 @@ export interface ITechProjectDatabase extends Omit Date: Fri, 14 Mar 2025 02:07:08 -0700 Subject: [PATCH 161/776] feat: track RoomChanges in clan log (#1174) Final part for clan log; closes #1152 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1174 --- .../contributeToDojoComponentController.ts | 7 +++- .../api/dojoComponentRushController.ts | 5 +++ src/controllers/api/getGuildLogController.ts | 7 ++++ .../api/startDojoRecipeController.ts | 20 +++++++++-- src/models/guildModel.ts | 15 +++++++- src/services/guildService.ts | 36 +++++++++++++++++-- src/types/guildTypes.ts | 6 ++++ 7 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 21be9c82..6f13ab54 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -4,7 +4,8 @@ import { getDojoClient, getGuildForRequestEx, processDojoBuildMaterialsGathered, - scaleRequiredCount + scaleRequiredCount, + setDojoRoomLogFunded } from "@/src/services/guildService"; import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -40,6 +41,10 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r } const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; processContribution(guild, request, inventory, inventoryChanges, meta, component); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (component.CompletionTime) { + setDojoRoomLogFunded(guild, component); + } } else { // Room is past "Collecting Materials" if (request.DecoId) { diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index e72531f4..934b8d98 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -35,6 +35,11 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { } else { const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; processContribution(component, meta, platinumDonated); + + const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); + if (entry) { + entry.dateTime = component.CompletionTime!; + } } await guild.save(); diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 9d4b82c5..47c94b63 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -18,6 +18,13 @@ export const getGuildLogController: RequestHandler = async (req, res) => { StandingsUpdates: [], ClassChanges: [] }; + guild.RoomChanges?.forEach(entry => { + log.RoomChanges.push({ + dateTime: toMongoDate(entry.dateTime), + entryType: entry.entryType, + details: entry.details + }); + }); guild.TechChanges?.forEach(entry => { log.TechChanges.push({ dateTime: toMongoDate(entry.dateTime), diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index ee7bb202..d492c09a 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -1,6 +1,11 @@ import { RequestHandler } from "express"; import { IDojoComponentClient } from "@/src/types/guildTypes"; -import { getDojoClient, getGuildForRequest, processDojoBuildMaterialsGathered } from "@/src/services/guildService"; +import { + getDojoClient, + getGuildForRequest, + processDojoBuildMaterialsGathered, + setDojoRoomLogFunded +} from "@/src/services/guildService"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; import { config } from "@/src/services/configService"; @@ -21,10 +26,20 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { guild.DojoEnergy += room.energy; } + const componentId = new Types.ObjectId(); + + guild.RoomChanges ??= []; + guild.RoomChanges.push({ + dateTime: new Date(), + entryType: 2, + details: request.PlacedComponent.pf, + componentId: componentId + }); + const component = guild.DojoComponents[ guild.DojoComponents.push({ - _id: new Types.ObjectId(), + _id: componentId, pf: request.PlacedComponent.pf, ppf: request.PlacedComponent.ppf, pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), @@ -38,6 +53,7 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { if (room) { processDojoBuildMaterialsGathered(guild, room); } + setDojoRoomLogFunded(guild, component); } await guild.save(); res.json(await getDojoClient(guild, 0)); diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 195fe611..b8eb97af 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -7,7 +7,8 @@ import { IGuildMemberDatabase, IGuildLogEntryNumber, IGuildLogEntryString, - IGuildRank + IGuildRank, + IGuildLogRoomChange } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -34,6 +35,7 @@ const dojoComponentSchema = new Schema({ RegularCredits: Number, MiscItems: { type: [typeCountSchema], default: undefined }, CompletionTime: Date, + CompletionLogPending: Boolean, RushPlatinum: Number, DestructionTime: Date, Decos: [dojoDecoSchema], @@ -115,6 +117,16 @@ const guildLogEntryStringSchema = new Schema( { _id: false } ); +const guildLogRoomChangeSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: String, + componentId: Types.ObjectId + }, + { _id: false } +); + const guildLogEntryNumberSchema = new Schema( { dateTime: Date, @@ -146,6 +158,7 @@ const guildSchema = new Schema( CeremonyContributors: { type: [Types.ObjectId], default: undefined }, CeremonyResetDate: Date, CeremonyEndo: Number, + RoomChanges: { type: [guildLogRoomChangeSchema], default: undefined }, TechChanges: { type: [guildLogEntryStringSchema], default: undefined }, RosterActivity: { type: [guildLogEntryStringSchema], default: undefined }, ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined } diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 061e685f..bb580383 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -6,6 +6,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { IDojoClient, IDojoComponentClient, + IDojoComponentDatabase, IDojoContributable, IDojoDecoClient, IGuildClient, @@ -126,7 +127,8 @@ export const getDojoClient = async ( DojoComponents: [] }; const roomsToRemove: Types.ObjectId[] = []; - guild.DojoComponents.forEach(dojoComponent => { + let needSave = false; + for (const dojoComponent of guild.DojoComponents) { if (!componentId || dojoComponent._id.equals(componentId)) { const clientComponent: IDojoComponentClient = { id: toOid(dojoComponent._id), @@ -143,10 +145,18 @@ export const getDojoClient = async ( } if (dojoComponent.CompletionTime) { clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + if (dojoComponent.CompletionLogPending && Date.now() >= dojoComponent.CompletionTime.getTime()) { + const entry = guild.RoomChanges?.find(x => x.componentId.equals(dojoComponent._id)); + if (entry) { + dojoComponent.CompletionLogPending = undefined; + entry.entryType = 1; + needSave = true; + } + } if (dojoComponent.DestructionTime) { if (Date.now() >= dojoComponent.DestructionTime.getTime()) { roomsToRemove.push(dojoComponent._id); - return; + continue; } clientComponent.DestructionTime = toMongoDate(dojoComponent.DestructionTime); } @@ -175,12 +185,15 @@ export const getDojoClient = async ( } dojo.DojoComponents.push(clientComponent); } - }); + } if (roomsToRemove.length) { logger.debug(`removing now-destroyed rooms`, roomsToRemove); for (const id of roomsToRemove) { removeDojoRoom(guild, id); } + needSave = true; + } + if (needSave) { await guild.save(); } return dojo; @@ -203,6 +216,13 @@ export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types } moveResourcesToVault(guild, component); component.Decos?.forEach(deco => moveResourcesToVault(guild, deco)); + + if (guild.RoomChanges) { + const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id)); + if (index != -1) { + guild.RoomChanges.splice(index, 1); + } + } }; export const removeDojoDeco = ( @@ -254,6 +274,16 @@ export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, } }; +// guild.save(); is expected some time after this function is called +export const setDojoRoomLogFunded = (guild: TGuildDatabaseDocument, component: IDojoComponentDatabase): void => { + const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); + if (entry && entry.entryType == 2) { + entry.entryType = 0; + entry.dateTime = component.CompletionTime!; + component.CompletionLogPending = true; + } +}; + export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClient): Promise => { const inventory = await getInventory(member._id.$oid, "PlayerLevel ActiveAvatarImageType"); member.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index e62cacc5..811f1813 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -48,6 +48,7 @@ export interface IGuildDatabase { CeremonyContributors?: Types.ObjectId[]; CeremonyResetDate?: Date; + RoomChanges?: IGuildLogRoomChange[]; TechChanges?: IGuildLogEntryString[]; RosterActivity?: IGuildLogEntryString[]; ClassChanges?: IGuildLogEntryNumber[]; @@ -146,6 +147,7 @@ export interface IDojoComponentDatabase _id: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; + CompletionLogPending?: boolean; DestructionTime?: Date; Decos?: IDojoDecoDatabase[]; } @@ -193,6 +195,10 @@ export interface IGuildLogEntryString { details: string; } +export interface IGuildLogRoomChange extends IGuildLogEntryString { + componentId: Types.ObjectId; +} + export interface IGuildLogEntryNumber { dateTime: Date; entryType: number; From 236cccc137f59aaf1ed3218cd94a4b0babcf7b93 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 14 Mar 2025 11:21:56 +0100 Subject: [PATCH 162/776] fix: remove dojo key after being kicked from clan --- src/controllers/api/removeFromGuildController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 3b886099..2ae0be1c 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -17,9 +17,9 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { inventory.GuildId = undefined; // Remove clan key or blueprint from kicked member - const itemIndex = inventory.MiscItems.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); + const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); if (itemIndex != -1) { - inventory.MiscItems.splice(itemIndex, 1); + inventory.LevelKeys.splice(itemIndex, 1); } else { const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); if (recipeIndex != -1) { From 0facdd1af94c8958883d919c4a648d6f00d33b9f Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 14 Mar 2025 07:09:28 -0700 Subject: [PATCH 163/776] chore: check permissions for various clan requests (#1175) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1175 --- .../api/abortDojoComponentController.ts | 24 +++++++++++-- ...abortDojoComponentDestructionController.ts | 13 +++++-- src/controllers/api/addToGuildController.ts | 10 +++--- .../api/changeDojoRootController.ts | 15 +++++--- .../api/changeGuildRankController.ts | 36 ++++++++++++------- .../contributeToDojoComponentController.ts | 7 +++- .../api/customizeGuildRanksController.ts | 11 ++++-- .../api/destroyDojoDecoController.ts | 19 ++++++++-- .../api/dojoComponentRushController.ts | 6 +++- src/controllers/api/guildTechController.ts | 28 ++++++++++++--- .../api/placeDecoInComponentController.ts | 14 ++++++-- ...queueDojoComponentDestructionController.ts | 13 +++++-- .../api/removeFromGuildController.ts | 25 +++++++------ src/controllers/api/setGuildMotdController.ts | 9 +++-- .../api/startDojoRecipeController.ts | 17 ++++++--- src/services/guildService.ts | 27 ++++++++++++++ 16 files changed, 216 insertions(+), 58 deletions(-) diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts index fbeb3670..0ad1f074 100644 --- a/src/controllers/api/abortDojoComponentController.ts +++ b/src/controllers/api/abortDojoComponentController.ts @@ -1,14 +1,34 @@ -import { getDojoClient, getGuildForRequestEx, removeDojoDeco, removeDojoRoom } from "@/src/services/guildService"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + removeDojoDeco, + removeDojoRoom +} from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const abortDojoComponentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest; + if ( + !hasAccessToDojo(inventory) || + !(await hasGuildPermission( + guild, + accountId, + request.DecoId ? GuildPermission.Decorator : GuildPermission.Architect + )) + ) { + res.json({ DojoRequestStatus: -1 }); + return; + } + if (request.DecoId) { removeDojoDeco(guild, request.ComponentId, request.DecoId); } else { diff --git a/src/controllers/api/abortDojoComponentDestructionController.ts b/src/controllers/api/abortDojoComponentDestructionController.ts index 1df71495..75f08f37 100644 --- a/src/controllers/api/abortDojoComponentDestructionController.ts +++ b/src/controllers/api/abortDojoComponentDestructionController.ts @@ -1,8 +1,17 @@ -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const componentId = req.query.componentId as string; guild.DojoComponents.id(componentId)!.DestructionTime = undefined; diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index a7a3f250..0d24cd00 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -1,11 +1,11 @@ import { Guild, GuildMember } from "@/src/models/guildModel"; import { Account } from "@/src/models/loginModel"; -import { fillInInventoryDataForGuildMember } from "@/src/services/guildService"; +import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService"; import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { IOid } from "@/src/types/commonTypes"; -import { IGuildMemberClient } from "@/src/types/guildTypes"; +import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; import { ExportFlavour } from "warframe-public-export-plus"; @@ -19,7 +19,10 @@ export const addToGuildController: RequestHandler = async (req, res) => { } const guild = (await Guild.findOne({ _id: payload.GuildId.$oid }, "Name"))!; - // TODO: Check sender is allowed to send invites for this guild. + const senderAccount = await getAccountForRequest(req); + if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { + res.status(400).json("Invalid permission"); + } if ( await GuildMember.exists({ @@ -37,7 +40,6 @@ export const addToGuildController: RequestHandler = async (req, res) => { status: 2 // outgoing invite }); - const senderAccount = await getAccountForRequest(req); const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); await createMessage(account._id.toString(), [ { diff --git a/src/controllers/api/changeDojoRootController.ts b/src/controllers/api/changeDojoRootController.ts index d596aae3..e28b92d9 100644 --- a/src/controllers/api/changeDojoRootController.ts +++ b/src/controllers/api/changeDojoRootController.ts @@ -1,12 +1,19 @@ import { RequestHandler } from "express"; -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; import { logger } from "@/src/utils/logger"; -import { IDojoComponentDatabase } from "@/src/types/guildTypes"; +import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes"; import { Types } from "mongoose"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; export const changeDojoRootController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); - // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the root. + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const idToNode: Record = {}; guild.DojoComponents.forEach(x => { diff --git a/src/controllers/api/changeGuildRankController.ts b/src/controllers/api/changeGuildRankController.ts index 3293d251..28a8113e 100644 --- a/src/controllers/api/changeGuildRankController.ts +++ b/src/controllers/api/changeGuildRankController.ts @@ -1,28 +1,38 @@ import { GuildMember } from "@/src/models/guildModel"; +import { getGuildForRequest, hasGuildPermissionEx } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const changeGuildRankController: RequestHandler = async (req, res) => { - // TODO: Verify permissions - const guildMember = (await GuildMember.findOne({ + const accountId = await getAccountIdForRequest(req); + const member = (await GuildMember.findOne({ + accountId: accountId, + guildId: req.query.guildId as string + }))!; + const newRank: number = parseInt(req.query.rankChange as string); + + const guild = await getGuildForRequest(req); + if (newRank < member.rank || !hasGuildPermissionEx(guild, member, GuildPermission.Promoter)) { + res.status(400).json("Invalid permission"); + return; + } + + const target = (await GuildMember.findOne({ guildId: req.query.guildId as string, accountId: req.query.targetId as string }))!; - guildMember.rank = parseInt(req.query.rankChange as string); - await guildMember.save(); + target.rank = parseInt(req.query.rankChange as string); + await target.save(); - if (guildMember.rank == 0) { + if (newRank == 0) { // If we just promoted someone else to Founding Warlord, we need to demote ourselves to Warlord. - await GuildMember.findOneAndUpdate( - { - guildId: req.query.guildId as string, - accountId: req.query.accountId as string - }, - { rank: 1 } - ); + member.rank = 1; + await member.save(); } res.json({ _id: req.query.targetId as string, - Rank: guildMember.rank + Rank: newRank }); }; diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 6f13ab54..9261627e 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -3,6 +3,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { getDojoClient, getGuildForRequestEx, + hasAccessToDojo, processDojoBuildMaterialsGathered, scaleRequiredCount, setDojoRoomLogFunded @@ -28,8 +29,12 @@ interface IContributeToDojoComponentRequest { export const contributeToDojoComponentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const guild = await getGuildForRequestEx(req, inventory); // Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in. + if (!hasAccessToDojo(inventory)) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; const component = guild.DojoComponents.id(request.ComponentId)!; diff --git a/src/controllers/api/customizeGuildRanksController.ts b/src/controllers/api/customizeGuildRanksController.ts index 3e237a81..5ddca7b6 100644 --- a/src/controllers/api/customizeGuildRanksController.ts +++ b/src/controllers/api/customizeGuildRanksController.ts @@ -1,11 +1,16 @@ -import { getGuildForRequest } from "@/src/services/guildService"; -import { IGuildRank } from "@/src/types/guildTypes"; +import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission, IGuildRank } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const customizeGuildRanksController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); const guild = await getGuildForRequest(req); const payload = JSON.parse(String(req.body)) as ICustomizeGuildRanksRequest; - // TODO: Verify permissions + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Ruler))) { + res.status(400).json("Invalid permission"); + return; + } guild.Ranks = payload.GuildRanks; await guild.save(); res.end(); diff --git a/src/controllers/api/destroyDojoDecoController.ts b/src/controllers/api/destroyDojoDecoController.ts index 1b7ec1dd..02323720 100644 --- a/src/controllers/api/destroyDojoDecoController.ts +++ b/src/controllers/api/destroyDojoDecoController.ts @@ -1,8 +1,23 @@ -import { getDojoClient, getGuildForRequest, removeDojoDeco } from "@/src/services/guildService"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + removeDojoDeco +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const destroyDojoDecoController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest; removeDojoDeco(guild, request.ComponentId, request.DecoId); diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 934b8d98..633b38e1 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -1,4 +1,4 @@ -import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IDojoContributable } from "@/src/types/guildTypes"; @@ -17,6 +17,10 @@ interface IDojoComponentRushRequest { export const dojoComponentRushController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); + if (!hasAccessToDojo(inventory)) { + res.json({ DojoRequestStatus: -1 }); + return; + } const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest; const component = guild.DojoComponents.id(request.ComponentId)!; diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index ff92244f..c51628af 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,11 @@ import { RequestHandler } from "express"; -import { getGuildForRequestEx, getGuildVault, scaleRequiredCount } from "@/src/services/guildService"; +import { + getGuildForRequestEx, + getGuildVault, + hasAccessToDojo, + hasGuildPermission, + scaleRequiredCount +} from "@/src/services/guildService"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { @@ -13,7 +19,7 @@ import { import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; -import { ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; +import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; import { TGuildDatabaseDocument } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; @@ -48,6 +54,10 @@ export const guildTechController: RequestHandler = async (req, res) => { } res.json({ TechProjects: techProjects }); } else if (action == "Start") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { + res.status(400).send("-1").end(); + return; + } const recipe = ExportDojoRecipes.research[data.RecipeType!]; guild.TechProjects ??= []; if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { @@ -71,6 +81,10 @@ export const guildTechController: RequestHandler = async (req, res) => { await guild.save(); res.end(); } else if (action == "Contribute") { + if (!hasAccessToDojo(inventory)) { + res.status(400).send("-1").end(); + return; + } const contributions = data as IGuildTechContributeFields; const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; @@ -133,9 +147,12 @@ export const guildTechController: RequestHandler = async (req, res) => { Vault: getGuildVault(guild) }); } else if (action == "Buy") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { + res.status(400).send("-1").end(); + return; + } const purchase = data as IGuildTechBuyFields; const quantity = parseInt(data.Action.split(",")[1]); - const inventory = await getInventory(accountId); const recipeChanges = [ { ItemType: purchase.RecipeType, @@ -157,9 +174,12 @@ export const guildTechController: RequestHandler = async (req, res) => { } }); } else if (action == "Fabricate") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { + res.status(400).send("-1").end(); + return; + } const payload = data as IGuildTechFabricateRequest; const recipe = ExportDojoRecipes.fabrications[payload.RecipeType]; - const inventory = await getInventory(accountId); const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false); inventoryChanges.MiscItems = recipe.ingredients.map(x => ({ ItemType: x.ItemType, diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index b08a1700..cc96a6b8 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -1,12 +1,20 @@ -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; export const placeDecoInComponentController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const request = JSON.parse(String(req.body)) as IPlaceDecoInComponentRequest; - // At this point, we know that a member of the guild is making this request. Assuming they are allowed to place decorations. const component = guild.DojoComponents.id(request.ComponentId)!; if (component.DecoCapacity === undefined) { diff --git a/src/controllers/api/queueDojoComponentDestructionController.ts b/src/controllers/api/queueDojoComponentDestructionController.ts index a4459cdd..361f91f8 100644 --- a/src/controllers/api/queueDojoComponentDestructionController.ts +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -1,9 +1,18 @@ import { config } from "@/src/services/configService"; -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const componentId = req.query.componentId as string; guild.DojoComponents.id(componentId)!.DestructionTime = new Date( diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 2ae0be1c..3a794634 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -1,15 +1,20 @@ import { GuildMember } from "@/src/models/guildModel"; import { Account } from "@/src/models/loginModel"; -import { getGuildForRequest } from "@/src/services/guildService"; +import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const removeFromGuildController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); const guild = await getGuildForRequest(req); - // TODO: Check permissions const payload = JSON.parse(String(req.body)) as IRemoveFromGuildRequest; + const isKick = !account._id.equals(payload.userId); + if (isKick && !(await hasGuildPermission(guild, account._id, GuildPermission.Regulator))) { + res.status(400).json("Invalid permission"); + return; + } const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!; if (guildMember.status == 0) { @@ -36,21 +41,19 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { await GuildMember.deleteOne({ _id: guildMember._id }); guild.RosterActivity ??= []; - if (account._id.equals(payload.userId)) { - // Leave - guild.RosterActivity.push({ - dateTime: new Date(), - entryType: 7, - details: getSuffixedName(account) - }); - } else { - // Kick + if (isKick) { const kickee = (await Account.findOne({ _id: payload.userId }))!; guild.RosterActivity.push({ dateTime: new Date(), entryType: 12, details: getSuffixedName(kickee) + "," + getSuffixedName(account) }); + } else { + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 7, + details: getSuffixedName(account) + }); } await guild.save(); diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts index 32f6f3ec..e0eb2aac 100644 --- a/src/controllers/api/setGuildMotdController.ts +++ b/src/controllers/api/setGuildMotdController.ts @@ -1,13 +1,18 @@ import { Guild } from "@/src/models/guildModel"; +import { hasGuildPermission } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const setGuildMotdController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); - const inventory = await getInventory(account._id.toString()); + const inventory = await getInventory(account._id.toString(), "GuildId"); const guild = (await Guild.findOne({ _id: inventory.GuildId! }))!; - // TODO: Check permissions + if (!(await hasGuildPermission(guild, account._id, GuildPermission.Herald))) { + res.status(400).json("Invalid permission"); + return; + } const IsLongMOTD = "longMOTD" in req.query; const MOTD = req.body ? String(req.body) : undefined; diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index d492c09a..04131d65 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -1,14 +1,18 @@ import { RequestHandler } from "express"; -import { IDojoComponentClient } from "@/src/types/guildTypes"; +import { GuildPermission, IDojoComponentClient } from "@/src/types/guildTypes"; import { getDojoClient, - getGuildForRequest, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, processDojoBuildMaterialsGathered, setDojoRoomLogFunded } from "@/src/services/guildService"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; import { config } from "@/src/services/configService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; interface IStartDojoRecipeRequest { PlacedComponent: IDojoComponentClient; @@ -16,8 +20,13 @@ interface IStartDojoRecipeRequest { } export const startDojoRecipeController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); - // At this point, we know that a member of the guild is making this request. Assuming they are allowed to start a build. + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const request = JSON.parse(String(req.body)) as IStartDojoRecipeRequest; const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == request.PlacedComponent.pf); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index bb580383..8a7e0220 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -4,6 +4,7 @@ import { addRecipes, getInventory } from "@/src/services/inventoryService"; import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { + GuildPermission, IDojoClient, IDojoComponentClient, IDojoComponentDatabase, @@ -11,6 +12,7 @@ import { IDojoDecoClient, IGuildClient, IGuildMemberClient, + IGuildMemberDatabase, IGuildVault } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; @@ -322,3 +324,28 @@ export const createUniqueClanName = async (name: string): Promise => { } while (discriminator != initialDiscriminator); throw new Error(`clan name is so unoriginal it's already been done 1000 times: ${name}`); }; + +export const hasAccessToDojo = (inventory: TInventoryDatabaseDocument): boolean => { + return inventory.LevelKeys.find(x => x.ItemType == "/Lotus/Types/Keys/DojoKey") !== undefined; +}; + +export const hasGuildPermission = async ( + guild: TGuildDatabaseDocument, + accountId: string | Types.ObjectId, + perm: GuildPermission +): Promise => { + const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id }); + if (member) { + return hasGuildPermissionEx(guild, member, perm); + } + return false; +}; + +export const hasGuildPermissionEx = ( + guild: TGuildDatabaseDocument, + member: IGuildMemberDatabase, + perm: GuildPermission +): boolean => { + const rank = guild.Ranks[member.rank]; + return (rank.Permissions & perm) != 0; +}; From 114e175efb3c84338bcbcc108c8f5973831bebc3 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 03:21:26 -0700 Subject: [PATCH 164/776] chore: set HWIDProtectEnabled so trading post can be used (#1182) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1182 --- src/controllers/api/inventoryController.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index acb7d2cd..4b7dfe28 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -228,6 +228,9 @@ export const getInventoryResponse = async ( // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId()); + // Set 2FA enabled so trading post can be used + inventoryResponse.HWIDProtectEnabled = true; + return inventoryResponse; }; From 25dfbf47248cb4b33539559e449ec0debfc5b42f Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 03:21:40 -0700 Subject: [PATCH 165/776] feat: edit clan tax rate (#1183) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1183 --- src/controllers/api/tradingController.ts | 23 +++++++++++++++++++++++ src/models/guildModel.ts | 1 + src/routes/api.ts | 2 ++ src/services/guildService.ts | 1 + src/types/guildTypes.ts | 7 +++---- 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/controllers/api/tradingController.ts diff --git a/src/controllers/api/tradingController.ts b/src/controllers/api/tradingController.ts new file mode 100644 index 00000000..af6e94f9 --- /dev/null +++ b/src/controllers/api/tradingController.ts @@ -0,0 +1,23 @@ +import { getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const tradingController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + const op = req.query.op as string; + if (op == "5") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) { + res.status(400).send("-1").end(); + return; + } + guild.TradeTax = parseInt(req.query.tax as string); + await guild.save(); + res.send(guild.TradeTax).end(); + } else { + throw new Error(`unknown trading op: ${op}`); + } +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index b8eb97af..00c428ed 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -142,6 +142,7 @@ const guildSchema = new Schema( MOTD: { type: String, default: "" }, LongMOTD: { type: longMOTDSchema, default: undefined }, Ranks: { type: [guildRankSchema], default: defaultRanks }, + TradeTax: { type: Number, default: 0 }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, diff --git a/src/routes/api.ts b/src/routes/api.ts index b2e27b53..5390d13a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -102,6 +102,7 @@ import { surveysController } from "@/src/controllers/api/surveysController"; import { syndicateSacrificeController } from "@/src/controllers/api/syndicateSacrificeController"; import { syndicateStandingBonusController } from "@/src/controllers/api/syndicateStandingBonusController"; import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController"; +import { tradingController } from "@/src/controllers/api/tradingController"; import { trainingResultController } from "@/src/controllers/api/trainingResultController"; import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController"; import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController"; @@ -153,6 +154,7 @@ apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController); apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController); apiRouter.get("/startLibraryPersonalTarget.php", startLibraryPersonalTargetController); apiRouter.get("/surveys.php", surveysController); +apiRouter.get("/trading.php", tradingController); apiRouter.get("/updateSession.php", updateSessionGetController); // post diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 8a7e0220..92f38e3e 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -90,6 +90,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s LongMOTD: guild.LongMOTD, Members: members, Ranks: guild.Ranks, + TradeTax: guild.TradeTax, Tier: 1, Vault: getGuildVault(guild), Class: guild.Class, diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 811f1813..93e8ae64 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -8,10 +8,8 @@ export interface IGuildClient { MOTD: string; LongMOTD?: ILongMOTD; Members: IGuildMemberClient[]; - Ranks: { - Name: string; - Permissions: number; - }[]; + Ranks: IGuildRank[]; + TradeTax: number; Tier: number; Vault: IGuildVault; Class: number; @@ -27,6 +25,7 @@ export interface IGuildDatabase { MOTD: string; LongMOTD?: ILongMOTD; Ranks: IGuildRank[]; + TradeTax: number; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; From db20369eb9c3867f6fb8d8bff3d6f106f974ebe0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 03:21:54 -0700 Subject: [PATCH 166/776] feat: add config options for event boosters (#1184) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1184 --- config.json.example | 3 ++ .../dynamic/worldStateController.ts | 50 +++++++++++++++++++ src/services/configService.ts | 3 ++ 3 files changed, 56 insertions(+) diff --git a/config.json.example b/config.json.example index f1c29035..85054641 100644 --- a/config.json.example +++ b/config.json.example @@ -38,6 +38,9 @@ "fastClanAscension": false, "spoofMasteryRank": -1, "events": { + "creditBoost": false, + "affinityBoost": false, + "resourceBoost": false, "starDays": true } } diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 3341992e..d995f5e8 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -17,6 +17,7 @@ export const worldStateController: RequestHandler = (req, res) => { : buildConfig.buildLabel, Time: Math.round(Date.now() / 1000), Goals: [], + GlobalUpgrades: [], EndlessXpChoices: [], ...staticWorldState }; @@ -76,6 +77,43 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; + if (config.events?.creditBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666672" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + if (config.events?.affinityBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666673" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + if (config.events?.resourceBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666674" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_PICKUP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + // Circuit choices cycling every week worldState.EndlessXpChoices.push({ Category: "EXC_NORMAL", @@ -158,6 +196,7 @@ interface IWorldState { Time: number; Goals: IGoal[]; SyndicateMissions: ISyndicateMission[]; + GlobalUpgrades: IGlobalUpgrade[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; KnownCalendarSeasons: ICalendarSeason[]; @@ -188,6 +227,17 @@ interface ISyndicateMission { Nodes: string[]; } +interface IGlobalUpgrade { + _id: IOid; + Activation: IMongoDate; + ExpiryDate: IMongoDate; + UpgradeType: string; + OperationType: string; + Value: number; + LocalizeTag: string; + LocalizeDescTag: string; +} + interface INodeOverride { _id: IOid; Activation?: IMongoDate; diff --git a/src/services/configService.ts b/src/services/configService.ts index 717dae35..b19ac57e 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -64,6 +64,9 @@ interface IConfig { fastClanAscension?: boolean; spoofMasteryRank?: number; events?: { + creditBoost?: boolean; + affinityBoost?: boolean; + resourceBoost?: boolean; starDays?: boolean; }; } From 2891e2fef5332a95f96195f8155b693c4c0847cc Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 03:24:39 -0700 Subject: [PATCH 167/776] chore: fix various eslint issues (#1176) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1176 --- src/controllers/api/giveQuestKey.ts | 2 +- src/controllers/api/inventoryController.ts | 1 + src/controllers/api/joinSessionController.ts | 11 +++-- src/managers/sessionManager.ts | 5 +- src/models/inventoryModels/loadoutModel.ts | 14 +++++- src/models/shipModel.ts | 10 +++- src/models/statsModel.ts | 4 +- src/services/loadoutService.ts | 4 +- src/services/missionInventoryUpdateService.ts | 21 +++++++-- src/services/personalRoomsService.ts | 5 +- src/services/purchaseService.ts | 11 +++-- src/services/questService.ts | 7 ++- src/services/shipService.ts | 19 ++------ src/services/statsService.ts | 12 ++--- src/types/personalRoomsTypes.ts | 14 +++++- src/types/session.ts | 46 +++++++++---------- 16 files changed, 111 insertions(+), 75 deletions(-) diff --git a/src/controllers/api/giveQuestKey.ts b/src/controllers/api/giveQuestKey.ts index 070dea75..8cd4b135 100644 --- a/src/controllers/api/giveQuestKey.ts +++ b/src/controllers/api/giveQuestKey.ts @@ -38,7 +38,7 @@ export interface IQuestKeyReward { Duration: number; CouponSku: number; Syndicate: string; - Milestones: any[]; + //Milestones: any[]; ChooseSetIndex: number; NewSystemReward: boolean; _id: IOid; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 4b7dfe28..53d554c7 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -261,6 +261,7 @@ const resourceGetParent = (resourceName: string): string | undefined => { if (resourceName in ExportResources) { return ExportResources[resourceName].parentName; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return ExportVirtuals[resourceName]?.parentName; }; diff --git a/src/controllers/api/joinSessionController.ts b/src/controllers/api/joinSessionController.ts index 4212c90f..7dff9fc9 100644 --- a/src/controllers/api/joinSessionController.ts +++ b/src/controllers/api/joinSessionController.ts @@ -2,12 +2,13 @@ import { RequestHandler } from "express"; import { getSessionByID } from "@/src/managers/sessionManager"; import { logger } from "@/src/utils/logger"; -const joinSessionController: RequestHandler = (_req, res) => { - const reqBody = JSON.parse(String(_req.body)); +export const joinSessionController: RequestHandler = (req, res) => { + const reqBody = JSON.parse(String(req.body)) as IJoinSessionRequest; logger.debug(`JoinSession Request`, { reqBody }); - const req = JSON.parse(String(_req.body)); - const session = getSessionByID(req.sessionIds[0] as string); + const session = getSessionByID(reqBody.sessionIds[0]); res.json({ rewardSeed: session?.rewardSeed, sessionId: { $oid: session?.sessionId } }); }; -export { joinSessionController }; +interface IJoinSessionRequest { + sessionIds: string[]; +} diff --git a/src/managers/sessionManager.ts b/src/managers/sessionManager.ts index 98bcc912..d933407a 100644 --- a/src/managers/sessionManager.ts +++ b/src/managers/sessionManager.ts @@ -44,7 +44,7 @@ function getSessionByID(sessionId: string): ISession | undefined { return sessions.find(session => session.sessionId === sessionId); } -function getSession(sessionIdOrRequest: string | IFindSessionRequest): any[] { +function getSession(sessionIdOrRequest: string | IFindSessionRequest): { createdBy: string; id: string }[] { if (typeof sessionIdOrRequest === "string") { const session = sessions.find(session => session.sessionId === sessionIdOrRequest); if (session) { @@ -107,8 +107,7 @@ function updateSession(sessionId: string, sessionData: string): boolean { const session = sessions.find(session => session.sessionId === sessionId); if (!session) return false; try { - const updatedData = JSON.parse(sessionData); - Object.assign(session, updatedData); + Object.assign(session, JSON.parse(sessionData)); return true; } catch (error) { console.error("Invalid JSON string for session update."); diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts index 8eba69c1..e43d33d1 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -1,7 +1,7 @@ import { IOid } from "@/src/types/commonTypes"; import { IEquipmentSelection } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ILoadoutConfigDatabase, ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; -import { Model, Schema, Types, model } from "mongoose"; +import { Document, Model, Schema, Types, model } from "mongoose"; const oidSchema = new Schema( { @@ -97,3 +97,15 @@ type loadoutDocumentProps = { type loadoutModelType = Model; export const Loadout = model("Loadout", loadoutSchema); + +// eslint-disable-next-line @typescript-eslint/ban-types +export type TLoadoutDatabaseDocument = Document & + Omit< + ILoadoutDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof loadoutDocumentProps + > & + loadoutDocumentProps; diff --git a/src/models/shipModel.ts b/src/models/shipModel.ts index 13e1ef53..784a6125 100644 --- a/src/models/shipModel.ts +++ b/src/models/shipModel.ts @@ -1,4 +1,4 @@ -import { Schema, model } from "mongoose"; +import { Document, Schema, Types, model } from "mongoose"; import { IShipDatabase } from "../types/shipTypes"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { colorSchema } from "@/src/models/inventoryModels/inventoryModel"; @@ -47,3 +47,11 @@ shipSchema.set("toObject", { }); export const Ship = model("Ships", shipSchema); + +// eslint-disable-next-line @typescript-eslint/ban-types +export type TShipDatabaseDocument = Document & + IShipDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }; diff --git a/src/models/statsModel.ts b/src/models/statsModel.ts index e11ca652..c8f5659b 100644 --- a/src/models/statsModel.ts +++ b/src/models/statsModel.ts @@ -4,7 +4,7 @@ import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase, const abilitySchema = new Schema( { type: { type: String, required: true }, - used: Number + used: { type: Number, required: true } }, { _id: false } ); @@ -32,7 +32,7 @@ const missionSchema = new Schema( const scanSchema = new Schema( { type: { type: String, required: true }, - scans: Number + scans: { type: Number, required: true } }, { _id: false } ); diff --git a/src/services/loadoutService.ts b/src/services/loadoutService.ts index f9c385c5..265a8150 100644 --- a/src/services/loadoutService.ts +++ b/src/services/loadoutService.ts @@ -1,6 +1,6 @@ -import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; +import { Loadout, TLoadoutDatabaseDocument } from "@/src/models/inventoryModels/loadoutModel"; -export const getLoadout = async (accountId: string) => { +export const getLoadout = async (accountId: string): Promise => { const loadout = await Loadout.findOne({ loadoutOwnerId: accountId }); if (!loadout) { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 71c2828e..fb4a936e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -313,6 +313,12 @@ export const addMissionInventoryUpdates = async ( return inventoryChanges; }; +interface AddMissionRewardsReturnType { + MissionRewards: IMissionReward[]; + inventoryChanges?: IInventoryChanges; + credits?: IMissionCredits; +} + //TODO: return type of partial missioninventoryupdate response export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, @@ -324,7 +330,7 @@ export const addMissionRewards = async ( VoidTearParticipantsCurrWave: voidTearWave, StrippedItems: strippedItems }: IMissionInventoryUpdateRequest -) => { +): Promise => { if (!rewardInfo) { //TODO: if there is a case where you can have credits collected during a mission but no rewardInfo, add credits needs to be handled earlier logger.debug(`Mission ${missions!.Tag} did not have Reward Info `); @@ -435,6 +441,13 @@ export const addMissionRewards = async ( return { inventoryChanges, MissionRewards, credits }; }; +interface IMissionCredits { + MissionCredits: number[]; + CreditBonus: number[]; + TotalCredits: number[]; + DailyMissionBonus?: boolean; +} + //creditBonus is not entirely accurate. //TODO: consider ActiveBoosters export const addCredits = ( @@ -444,11 +457,11 @@ export const addCredits = ( missionCompletionCredits, rngRewardCredits }: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number } -) => { +): IMissionCredits => { const hasDailyCreditBonus = true; const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits; - const finalCredits = { + const finalCredits: IMissionCredits = { MissionCredits: [missionDropCredits, missionDropCredits], CreditBonus: [missionCompletionCredits, missionCompletionCredits], TotalCredits: [totalCredits, totalCredits] @@ -471,7 +484,7 @@ export const addFixedLevelRewards = ( rewards: IMissionRewardExternal, inventory: TInventoryDatabaseDocument, MissionRewards: IMissionReward[] -) => { +): number => { let missionBonusCredits = 0; if (rewards.credits) { missionBonusCredits += rewards.credits; diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 5325af1c..24399655 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -1,7 +1,8 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { addItem, getInventory } from "@/src/services/inventoryService"; +import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes"; -export const getPersonalRooms = async (accountId: string) => { +export const getPersonalRooms = async (accountId: string): Promise => { const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); if (!personalRooms) { @@ -10,7 +11,7 @@ export const getPersonalRooms = async (accountId: string) => { return personalRooms; }; -export const updateShipFeature = async (accountId: string, shipFeature: string) => { +export const updateShipFeature = async (accountId: string, shipFeature: string): Promise => { const personalRooms = await getPersonalRooms(accountId); if (personalRooms.Ship.Features.includes(shipFeature)) { diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index d960c629..b3d795a3 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -108,8 +108,11 @@ export const handlePurchase = async ( ]; } purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; - } else if (!ExportVendors[purchaseRequest.PurchaseParams.SourceId!]) { - throw new Error(`unknown vendor: ${purchaseRequest.PurchaseParams.SourceId!}`); + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!ExportVendors[purchaseRequest.PurchaseParams.SourceId!]) { + throw new Error(`unknown vendor: ${purchaseRequest.PurchaseParams.SourceId!}`); + } } } @@ -120,8 +123,6 @@ export const handlePurchase = async ( ); combineInventoryChanges(purchaseResponse.InventoryChanges, inventoryChanges); - if (!purchaseResponse) throw new Error("purchase response was undefined"); - const currencyChanges = updateCurrency( inventory, purchaseRequest.PurchaseParams.ExpectedPrice, @@ -149,6 +150,7 @@ export const handlePurchase = async ( ]; } else { const syndicate = ExportSyndicates[syndicateTag]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (syndicate) { const favour = syndicate.favours.find( x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem @@ -360,6 +362,7 @@ const handleBoosterPackPurchase = async ( quantity: number ): Promise => { const pack = ExportBoosterPacks[typeName]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!pack) { throw new Error(`unknown booster pack: ${typeName}`); } diff --git a/src/services/questService.ts b/src/services/questService.ts index 5e70f502..a9629339 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -93,7 +93,10 @@ export const updateQuestStage = ( Object.assign(questStage, questStageUpdate); }; -export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase) => { +export const addQuestKey = ( + inventory: TInventoryDatabaseDocument, + questKey: IQuestKeyDatabase +): IQuestKeyClient | undefined => { if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) { logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`); return; @@ -115,7 +118,7 @@ export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQu return inventory.QuestKeys[index - 1].toJSON(); }; -export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string) => { +export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string): Promise => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[questKey]?.chainStages; diff --git a/src/services/shipService.ts b/src/services/shipService.ts index 0925a409..cc97d52d 100644 --- a/src/services/shipService.ts +++ b/src/services/shipService.ts @@ -1,11 +1,10 @@ -import { Ship } from "@/src/models/shipModel"; -import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; +import { Ship, TShipDatabaseDocument } from "@/src/models/shipModel"; import { Types } from "mongoose"; export const createShip = async ( accountOwnerId: Types.ObjectId, typeName: string = "/Lotus/Types/Items/Ships/DefaultShip" -) => { +): Promise => { try { const ship = new Ship({ ItemType: typeName, @@ -21,7 +20,7 @@ export const createShip = async ( } }; -export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = "") => { +export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = ""): Promise => { const ship = await Ship.findOne({ _id: shipId }, fieldSelection); if (!ship) { @@ -30,15 +29,3 @@ export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = " return ship; }; - -export const getShipLean = async (shipOwnerId: string) => { - const ship = await Ship.findOne({ ShipOwnerId: shipOwnerId }).lean().populate<{ - LoadOutInventory: { LoadOutPresets: ILoadoutDatabase }; - }>("LoadOutInventory.LoadOutPresets"); - - if (!ship) { - throw new Error(`error finding a ship for account ${shipOwnerId}`); - } - - return ship; -}; diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 49bcea3f..75a05627 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -82,7 +82,6 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: for (const [type, scans] of Object.entries(data as IUploadEntry)) { const scan = playerStats.Scans.find(element => element.type === type); if (scan) { - scan.scans ??= 0; scan.scans += scans; } else { playerStats.Scans.push({ type: type, scans }); @@ -95,7 +94,6 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: for (const [type, used] of Object.entries(data as IUploadEntry)) { const ability = playerStats.Abilities.find(element => element.type === type); if (ability) { - ability.used ??= 0; ability.used += used; } else { playerStats.Abilities.push({ type: type, used }); @@ -307,22 +305,20 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: for (const [category, value] of Object.entries(actionData as IStatsSet)) { switch (category) { case "ELO_RATING": - playerStats.Rating = value; + playerStats.Rating = value as number; break; case "RANK": - playerStats.Rank = value; + playerStats.Rank = value as number; break; case "PLAYER_LEVEL": - playerStats.PlayerLevel = value; + playerStats.PlayerLevel = value as number; break; default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index f91a5b88..cfb98ae7 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -7,7 +7,7 @@ import { ITailorShopDatabase, TBootLocation } from "@/src/types/shipTypes"; -import { Model, Types } from "mongoose"; +import { Document, Model, Types } from "mongoose"; export interface IOrbiter { Features: string[]; @@ -48,3 +48,15 @@ export type PersonalRoomsDocumentProps = { // eslint-disable-next-line @typescript-eslint/ban-types export type PersonalRoomsModelType = Model; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type TPersonalRoomsDatabaseDocument = Document & + Omit< + IPersonalRoomsDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof PersonalRoomsDocumentProps + > & + PersonalRoomsDocumentProps; diff --git a/src/types/session.ts b/src/types/session.ts index 2e534f70..d7f6e68a 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -1,29 +1,29 @@ export interface ISession { sessionId: string; creatorId: string; - maxPlayers: number; - minPlayers: number; - privateSlots: number; - scoreLimit: number; - timeLimit: number; - gameModeId: number; - eloRating: number; - regionId: number; - difficulty: number; - hasStarted: boolean; - enableVoice: boolean; - matchType: string; - maps: string[]; - originalSessionId: string; - customSettings: string; - rewardSeed: number; - guildId: string; - buildId: number; - platform: number; - xplatform: boolean; - freePublic: number; - freePrivate: number; - fullReset: number; + maxPlayers?: number; + minPlayers?: number; + privateSlots?: number; + scoreLimit?: number; + timeLimit?: number; + gameModeId?: number; + eloRating?: number; + regionId?: number; + difficulty?: number; + hasStarted?: boolean; + enableVoice?: boolean; + matchType?: string; + maps?: string[]; + originalSessionId?: string; + customSettings?: string; + rewardSeed?: number; + guildId?: string; + buildId?: number; + platform?: number; + xplatform?: boolean; + freePublic?: number; + freePrivate?: number; + fullReset?: number; } export interface IFindSessionRequest { From ae9a98ca8bb7b610c7622ebf12a51b3bdeea8c33 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 15 Mar 2025 03:25:02 -0700 Subject: [PATCH 168/776] fix(stats): handle eidolon capture (#1190) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1190 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/statsService.ts | 33 +++++++++++++++++++++++++-------- src/types/statTypes.ts | 3 +++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 75a05627..ebc5b2f2 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -104,14 +104,16 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "FIRE_WEAPON": case "HIT_ENTITY_ITEM": case "HEADSHOT_ITEM": - case "KILL_ENEMY_ITEM": { + case "KILL_ENEMY_ITEM": + case "KILL_ASSIST_ITEM": { playerStats.Weapons ??= []; const statKey = { FIRE_WEAPON: "fired", HIT_ENTITY_ITEM: "hits", HEADSHOT_ITEM: "headshots", - KILL_ENEMY_ITEM: "kills" - }[category] as "fired" | "hits" | "headshots" | "kills"; + KILL_ENEMY_ITEM: "kills", + KILL_ASSIST_ITEM: "assists" + }[category] as "fired" | "hits" | "headshots" | "kills" | "assists"; for (const [type, count] of Object.entries(data as IUploadEntry)) { const weapon = playerStats.Weapons.find(element => element.type === type); @@ -129,19 +131,33 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "KILL_ENEMY": case "EXECUTE_ENEMY": - case "HEADSHOT": { + case "HEADSHOT": + case "KILL_ASSIST": { playerStats.Enemies ??= []; const enemyStatKey = { KILL_ENEMY: "kills", EXECUTE_ENEMY: "executions", - HEADSHOT: "headshots" - }[category] as "kills" | "executions" | "headshots"; + HEADSHOT: "headshots", + KILL_ASSIST: "assists" + }[category] as "kills" | "executions" | "headshots" | "assists"; for (const [type, count] of Object.entries(data as IUploadEntry)) { const enemy = playerStats.Enemies.find(element => element.type === type); if (enemy) { - enemy[enemyStatKey] ??= 0; - enemy[enemyStatKey] += count; + if (category === "KILL_ENEMY") { + enemy.kills ??= 0; + const captureCount = (actionData["CAPTURE_ENEMY"] as IUploadEntry)?.[type]; + if (captureCount) { + enemy.kills += Math.max(count - captureCount, 0); + enemy.captures ??= 0; + enemy.captures += captureCount; + } else { + enemy.kills += count; + } + } else { + enemy[enemyStatKey] ??= 0; + enemy[enemyStatKey] += count; + } } else { const newEnemy: IEnemy = { type: type }; newEnemy[enemyStatKey] = count; @@ -394,6 +410,7 @@ const ignoredCategories = [ "PRE_DIE_ITEM", "GEAR_USED", "DIE_ITEM", + "CAPTURE_ENEMY", // handled in KILL_ENEMY // timers action "IN_SHIP_TIME", diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts index 0b49e20e..6d3a2fd3 100644 --- a/src/types/statTypes.ts +++ b/src/types/statTypes.ts @@ -44,6 +44,7 @@ export interface IEnemy { kills?: number; assists?: number; deaths?: number; + captures?: number; } export interface IMission { @@ -126,6 +127,8 @@ export interface IStatsAdd { DIE_ITEM?: IUploadEntry; EXECUTE_ENEMY?: IUploadEntry; EXECUTE_ENEMY_ITEM?: IUploadEntry; + KILL_ASSIST?: IUploadEntry; + KILL_ASSIST_ITEM?: IUploadEntry; } export interface IUploadEntry { From 294bedd29a579a51564553a9fb6bee9d14ae4fb1 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 15 Mar 2025 03:48:47 -0700 Subject: [PATCH 169/776] fix(stats): add captures to stat model (#1191) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1191 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/models/statsModel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/statsModel.ts b/src/models/statsModel.ts index c8f5659b..c4ec87c3 100644 --- a/src/models/statsModel.ts +++ b/src/models/statsModel.ts @@ -16,7 +16,8 @@ const enemySchema = new Schema( headshots: Number, kills: Number, assists: Number, - deaths: Number + deaths: Number, + captures: Number }, { _id: false } ); From 2f59b3d775d5b88f45a28ab6cb7400b4edd36411 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sat, 15 Mar 2025 06:33:21 -0700 Subject: [PATCH 170/776] chore(webui): update to German and Chinese translation file (#1196) - Translated the latest new vendor string into German in `de.js` - Added the `[UNTRANSLATED] No Vendor Purchase Limits` placeholder string in `zh.js` because that file was missed. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1196 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- static/webui/translations/zh.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 0a329181..0d6d4c9f 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -110,7 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, - cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 2858309e..a783bb04 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `全物品自带适配器`, cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, cheats_noDailyStandingLimits: `无每日声望限制`, + cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, cheats_fastDojoRoomDestruction: `快速拆除道场房间`, From 2d6e096fdeff09402744f3c5acb967b1374a62f9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 06:39:54 -0700 Subject: [PATCH 171/776] feat: argon crystal decay (#1195) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1195 --- config.json.example | 1 + src/controllers/api/inventoryController.ts | 57 +++++++++++++++----- src/models/inventoryModels/inventoryModel.ts | 8 ++- src/models/loginModel.ts | 1 - src/services/configService.ts | 1 + src/services/inventoryService.ts | 16 ++++++ src/types/inventoryTypes/inventoryTypes.ts | 4 +- src/types/loginTypes.ts | 1 - static/webui/index.html | 4 ++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 14 files changed, 81 insertions(+), 17 deletions(-) diff --git a/config.json.example b/config.json.example index 85054641..c8a2df6a 100644 --- a/config.json.example +++ b/config.json.example @@ -29,6 +29,7 @@ "unlockExilusEverywhere": false, "unlockArcanesEverywhere": false, "noDailyStandingLimits": false, + "noArgonCrystalDecay": false, "noVendorPurchaseLimits": true, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": false, diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 53d554c7..40d13d4c 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getAccountForRequest } from "@/src/services/loginService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; import allDialogue from "@/static/fixed_responses/allDialogue.json"; @@ -14,12 +14,13 @@ import { ExportVirtuals } from "warframe-public-export-plus"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; -import { allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; +import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; +import { logger } from "@/src/utils/logger"; export const inventoryController: RequestHandler = async (request, response) => { - const account = await getAccountForRequest(request); + const accountId = await getAccountIdForRequest(request); - const inventory = await Inventory.findOne({ accountOwnerId: account._id.toString() }); + const inventory = await Inventory.findOne({ accountOwnerId: accountId }); if (!inventory) { response.status(400).json({ error: "inventory was undefined" }); @@ -27,11 +28,7 @@ export const inventoryController: RequestHandler = async (request, response) => } // Handle daily reset - const today: number = Math.trunc(new Date().getTime() / 86400000); - if (account.LastLoginDay != today) { - account.LastLoginDay = today; - await account.save(); - + if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { for (const key of allDailyAffiliationKeys) { inventory[key] = 16000 + inventory.PlayerLevel * 500; } @@ -39,6 +36,45 @@ export const inventoryController: RequestHandler = async (request, response) => inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); + if (inventory.NextRefill) { + if (config.noArgonCrystalDecay) { + inventory.FoundToday = undefined; + } else { + const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1; + const today = Math.trunc(Date.now() / 86400000); + const daysPassed = today - lastLoginDay; + for (let i = 0; i != daysPassed; ++i) { + const numArgonCrystals = + inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") + ?.ItemCount ?? 0; + if (numArgonCrystals == 0) { + break; + } + const numStableArgonCrystals = + inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") + ?.ItemCount ?? 0; + const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals; + const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2); + logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, { + numArgonCrystals, + numStableArgonCrystals, + numDecayingArgonCrystals, + numDecayingArgonCrystalsToRemove + }); + // Remove half of owned decaying argon crystals + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal", + ItemCount: numDecayingArgonCrystalsToRemove * -1 + } + ]); + // All stable argon crystals are now decaying + inventory.FoundToday = undefined; + } + } + } + + inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); await inventory.save(); } @@ -219,9 +255,6 @@ export const getInventoryResponse = async ( applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); } - // Fix for #380 - inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } }; - // This determines if the "void fissures" tab is shown in navigation. inventoryResponse.HasOwnedVoidProjectionsPreviously = true; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index c7446f41..c0bdcb58 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1161,7 +1161,8 @@ const inventorySchema = new Schema( ChallengeProgress: [challengeProgressSchema], //Account Item like Ferrite,Form,Kuva etc - MiscItems: [typeCountSchema], + MiscItems: { type: [typeCountSchema], default: [] }, + FoundToday: { type: [typeCountSchema], default: undefined }, //Non Upgrade Mods Example:I have 999 item WeaponElectricityDamageMod (only "ItemCount"+"ItemType") RawUpgrades: [RawUpgrades], @@ -1360,7 +1361,7 @@ const inventorySchema = new Schema( //https://warframe.fandom.com/wiki/Helminth InfestedFoundry: infestedFoundrySchema, - NextRefill: Schema.Types.Mixed, // Date, convert to IMongoDate + NextRefill: { type: Date, default: undefined }, //Purchase this new permanent skin from the Lotus customization options in Personal Quarters located in your Orbiter. //https://warframe.fandom.com/wiki/Lotus#The_New_War @@ -1435,6 +1436,9 @@ inventorySchema.set("toJSON", { if (inventoryDatabase.BlessingCooldown) { inventoryResponse.BlessingCooldown = toMongoDate(inventoryDatabase.BlessingCooldown); } + if (inventoryDatabase.NextRefill) { + inventoryResponse.NextRefill = toMongoDate(inventoryDatabase.NextRefill); + } } }); diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 75a12356..47cbe8ff 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -21,7 +21,6 @@ const databaseAccountSchema = new Schema( TrackedSettings: { type: [String], default: [] }, Nonce: { type: Number, default: 0 }, Dropped: Boolean, - LastLoginDay: { type: Number }, LatestEventMessageDate: { type: Date, default: 0 } }, opts diff --git a/src/services/configService.ts b/src/services/configService.ts index b19ac57e..66c50dda 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -55,6 +55,7 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noArgonCrystalDecay?: boolean; noVendorPurchaseLimits?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 42e0fa66..cac1254d 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1045,6 +1045,22 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: } MiscItems[itemIndex].ItemCount += ItemCount; + + if (ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") { + inventory.FoundToday ??= []; + let foundTodayIndex = inventory.FoundToday.findIndex(x => x.ItemType == ItemType); + if (foundTodayIndex == -1) { + foundTodayIndex = inventory.FoundToday.push({ ItemType, ItemCount: 0 }) - 1; + } + inventory.FoundToday[foundTodayIndex].ItemCount += ItemCount; + if (inventory.FoundToday[foundTodayIndex].ItemCount <= 0) { + inventory.FoundToday.splice(foundTodayIndex, 1); + } + if (inventory.FoundToday.length == 0) { + inventory.FoundToday = undefined; + } + } + if (MiscItems[itemIndex].ItemCount == 0) { MiscItems.splice(itemIndex, 1); } else if (MiscItems[itemIndex].ItemCount <= 0) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 6fff6735..70d070b0 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -42,6 +42,7 @@ export interface IInventoryDatabase | "PendingCoupon" | "Drones" | "RecentVendorPurchases" + | "NextRefill" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -69,6 +70,7 @@ export interface IInventoryDatabase PendingCoupon?: IPendingCouponDatabase; Drones: IDroneDatabase[]; RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; + NextRefill?: Date; } export interface IQuestKeyDatabase { @@ -307,7 +309,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; LastInventorySync: IOid; - NextRefill: IMongoDate; // Next time argon crystals will have a decay tick + NextRefill?: IMongoDate; FoundToday?: IMiscItem[]; // for Argon Crystals CustomMarkers?: ICustomMarkers[]; ActiveLandscapeTraps: any[]; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 108b0417..0aaf2eed 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -15,7 +15,6 @@ export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { email: string; password: string; Dropped?: boolean; - LastLoginDay?: number; LatestEventMessageDate: Date; } diff --git a/static/webui/index.html b/static/webui/index.html index bd5f13cc..540378d9 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -517,6 +517,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 0d6d4c9f..aefd1f84 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, + cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 455ab2e4..b60ba241 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -109,6 +109,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 9840911a..96b52eaa 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, + cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 030c704b..502b84af 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index a783bb04..57207f0a 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -110,6 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `全物品自带适配器`, cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, cheats_noDailyStandingLimits: `无每日声望限制`, + cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, From adddc11b6fa79963b4c10ffc27467b8aea76ed0a Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 10:25:15 -0700 Subject: [PATCH 172/776] fix: limit booster pack purchases to a max quantity of 100 (#1189) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1189 --- src/services/purchaseService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index b3d795a3..9336f0bf 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -370,6 +370,11 @@ const handleBoosterPackPurchase = async ( BoosterPackItems: "", InventoryChanges: {} }; + if (quantity > 100) { + throw new Error( + "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server." + ); + } for (let i = 0; i != quantity; ++i) { for (const weights of pack.rarityWeightsPerRoll) { const result = getRandomWeightedRewardUc(pack.components, weights); From 56fecef1bf2fb182ee7df5313eb3996b38d5f61d Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 15 Mar 2025 10:25:32 -0700 Subject: [PATCH 173/776] chore: set HasOwnedVoidProjectionsPreviously when acquiring a relic (#1198) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1198 --- src/controllers/api/inventoryController.ts | 3 --- src/models/inventoryModels/inventoryModel.ts | 1 + src/services/inventoryService.ts | 1 + src/types/inventoryTypes/inventoryTypes.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 40d13d4c..77f7bb1c 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -255,9 +255,6 @@ export const getInventoryResponse = async ( applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); } - // This determines if the "void fissures" tab is shown in navigation. - inventoryResponse.HasOwnedVoidProjectionsPreviously = true; - // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId()); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index c0bdcb58..b0a84f5f 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1186,6 +1186,7 @@ const inventorySchema = new Schema( ReceivedStartingGear: Boolean, ArchwingEnabled: Boolean, + HasOwnedVoidProjectionsPreviously: Boolean, //Use Operator\Drifter UseAdultOperatorLoadout: Boolean, diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index cac1254d..0408115c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -536,6 +536,7 @@ export const addItem = async ( } satisfies IMiscItem ]; addMiscItems(inventory, miscItemChanges); + inventory.HasOwnedVoidProjectionsPreviously = true; return { InventoryChanges: { MiscItems: miscItemChanges diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 70d070b0..2aee2a67 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -252,7 +252,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Affiliations: IAffiliation[]; QualifyingInvasions: any[]; FactionScores: number[]; - ArchwingEnabled: boolean; + ArchwingEnabled?: boolean; PendingSpectreLoadouts?: ISpectreLoadout[]; SpectreLoadouts?: ISpectreLoadout[]; EmailItems: ITypeCount[]; From 56a372ee6f9b212bc59a2afcf304095c4ac86428 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sat, 15 Mar 2025 12:44:46 -0700 Subject: [PATCH 174/776] chore(webui): update to German translation (#1204) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1204 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index aefd1f84..aff0d599 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -110,7 +110,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, - cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, + cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, From ab11f67f0bfb590d0c99ebf6ddc40abe64019c59 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 04:32:11 -0700 Subject: [PATCH 175/776] feat: clan polychrome research (#1177) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1177 --- src/controllers/api/guildTechController.ts | 86 +++++++++++++++------- src/models/guildModel.ts | 1 + src/services/guildService.ts | 15 ++++ src/types/guildTypes.ts | 2 + 4 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index c51628af..f9a2e9fb 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -4,6 +4,7 @@ import { getGuildVault, hasAccessToDojo, hasGuildPermission, + removePigmentsFromGuildMembers, scaleRequiredCount } from "@/src/services/guildService"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; @@ -28,8 +29,7 @@ export const guildTechController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); const guild = await getGuildForRequestEx(req, inventory); const data = JSON.parse(String(req.body)) as TGuildTechRequest; - const action = data.Action.split(",")[0]; - if (action == "Sync") { + if (data.Action == "Sync") { let needSave = false; const techProjects: ITechProjectClient[] = []; if (guild.TechProjects) { @@ -53,18 +53,18 @@ export const guildTechController: RequestHandler = async (req, res) => { await guild.save(); } res.json({ TechProjects: techProjects }); - } else if (action == "Start") { - if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { + } else if (data.Action == "Start") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; } - const recipe = ExportDojoRecipes.research[data.RecipeType!]; + const recipe = ExportDojoRecipes.research[data.RecipeType]; guild.TechProjects ??= []; if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { const techProject = guild.TechProjects[ guild.TechProjects.push({ - ItemType: data.RecipeType!, + ItemType: data.RecipeType, ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price), ReqItems: recipe.ingredients.map(x => ({ ItemType: x.ItemType, @@ -76,16 +76,20 @@ export const guildTechController: RequestHandler = async (req, res) => { setTechLogState(guild, techProject.ItemType, 5); if (config.noDojoResearchCosts) { processFundedProject(guild, techProject, recipe); + } else { + if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { + guild.ActiveDojoColorResearch = data.RecipeType; + } } } await guild.save(); res.end(); - } else if (action == "Contribute") { + } else if (data.Action == "Contribute") { if (!hasAccessToDojo(inventory)) { res.status(400).send("-1").end(); return; } - const contributions = data as IGuildTechContributeFields; + const contributions = data; const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; if (contributions.VaultCredits) { @@ -136,8 +140,12 @@ export const guildTechController: RequestHandler = async (req, res) => { if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { // This research is now fully funded. - const recipe = ExportDojoRecipes.research[data.RecipeType!]; + const recipe = ExportDojoRecipes.research[data.RecipeType]; processFundedProject(guild, techProject, recipe); + if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { + guild.ActiveDojoColorResearch = ""; + await removePigmentsFromGuildMembers(guild._id); + } } await guild.save(); @@ -146,12 +154,12 @@ export const guildTechController: RequestHandler = async (req, res) => { InventoryChanges: inventoryChanges, Vault: getGuildVault(guild) }); - } else if (action == "Buy") { + } else if (data.Action.split(",")[0] == "Buy") { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; } - const purchase = data as IGuildTechBuyFields; + const purchase = data as IGuildTechBuyRequest; const quantity = parseInt(data.Action.split(",")[1]); const recipeChanges = [ { @@ -173,13 +181,12 @@ export const guildTechController: RequestHandler = async (req, res) => { Recipes: recipeChanges } }); - } else if (action == "Fabricate") { + } else if (data.Action == "Fabricate") { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; } - const payload = data as IGuildTechFabricateRequest; - const recipe = ExportDojoRecipes.fabrications[payload.RecipeType]; + const recipe = ExportDojoRecipes.fabrications[data.RecipeType]; const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false); inventoryChanges.MiscItems = recipe.ingredients.map(x => ({ ItemType: x.ItemType, @@ -190,6 +197,31 @@ export const guildTechController: RequestHandler = async (req, res) => { await inventory.save(); // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. res.json({ inventoryChanges: inventoryChanges }); + } else if (data.Action == "Pause") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + project.State = -2; + guild.ActiveDojoColorResearch = ""; + await guild.save(); + await removePigmentsFromGuildMembers(guild._id); + res.end(); + } else if (data.Action == "Unpause") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + project.State = 0; + guild.ActiveDojoColorResearch = data.RecipeType; + const entry = guild.TechChanges?.find(x => x.details == data.RecipeType); + if (entry) { + entry.dateTime = new Date(); + } + await guild.save(); + res.end(); } else { throw new Error(`unknown guildTech action: ${data.Action}`); } @@ -233,20 +265,24 @@ const setTechLogState = ( }; type TGuildTechRequest = - | ({ - Action: string; - } & Partial & - Partial) - | IGuildTechFabricateRequest; + | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } + | IGuildTechBasicRequest + | IGuildTechContributeRequest; -interface IGuildTechStartFields { +interface IGuildTechBasicRequest { + Action: "Start" | "Fabricate" | "Pause" | "Unpause"; Mode: "Guild"; RecipeType: string; } -type IGuildTechBuyFields = IGuildTechStartFields; +interface IGuildTechBuyRequest { + Action: string; + Mode: "Guild"; + RecipeType: string; +} -interface IGuildTechContributeFields { +interface IGuildTechContributeRequest { + Action: "Contribute"; ResearchId: ""; RecipeType: string; RegularCredits: number; @@ -254,9 +290,3 @@ interface IGuildTechContributeFields { VaultCredits: number; VaultMiscItems: IMiscItem[]; } - -interface IGuildTechFabricateRequest { - Action: "Fabricate"; - Mode: "Guild"; - RecipeType: string; -} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 00c428ed..23d28b36 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -152,6 +152,7 @@ const guildSchema = new Schema( VaultShipDecorations: { type: [typeCountSchema], default: undefined }, VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined }, TechProjects: { type: [techProjectSchema], default: undefined }, + ActiveDojoColorResearch: { type: String, default: "" }, Class: { type: Number, default: 0 }, XP: { type: Number, default: 0 }, ClaimedXP: { type: [String], default: undefined }, diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 92f38e3e..9176e66b 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -93,6 +93,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s TradeTax: guild.TradeTax, Tier: 1, Vault: getGuildVault(guild), + ActiveDojoColorResearch: guild.ActiveDojoColorResearch, Class: guild.Class, XP: guild.XP, IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), @@ -350,3 +351,17 @@ export const hasGuildPermissionEx = ( const rank = guild.Ranks[member.rank]; return (rank.Permissions & perm) != 0; }; + +export const removePigmentsFromGuildMembers = async (guildId: string | Types.ObjectId): Promise => { + const members = await GuildMember.find({ guildId, status: 0 }, "accountId"); + for (const member of members) { + const inventory = await getInventory(member.accountId.toString(), "MiscItems"); + const index = inventory.MiscItems.findIndex( + x => x.ItemType == "/Lotus/Types/Items/Research/DojoColors/GenericDojoColorPigment" + ); + if (index != -1) { + inventory.MiscItems.splice(index, 1); + await inventory.save(); + } + } +}; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 93e8ae64..ee10656b 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -12,6 +12,7 @@ export interface IGuildClient { TradeTax: number; Tier: number; Vault: IGuildVault; + ActiveDojoColorResearch: string; Class: number; XP: number; IsContributor: boolean; @@ -38,6 +39,7 @@ export interface IGuildDatabase { VaultFusionTreasures?: IFusionTreasure[]; TechProjects?: ITechProjectDatabase[]; + ActiveDojoColorResearch: string; Class: number; XP: number; From ecc2e355354472087258ecc6998901be844a4808 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 04:32:57 -0700 Subject: [PATCH 176/776] feat: randomly generate daily modular weapon sales (#1199) Re #685 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1199 --- package-lock.json | 8 +- package.json | 2 +- .../api/modularWeaponSaleController.ts | 123 +++++++++++++++++- src/services/rngService.ts | 23 ++++ static/fixed_responses/modularWeaponSale.json | 86 ------------ 5 files changed, 147 insertions(+), 95 deletions(-) delete mode 100644 static/fixed_responses/modularWeaponSale.json diff --git a/package-lock.json b/package-lock.json index c7a9d1bf..9dacff87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.44", + "warframe-public-export-plus": "^0.5.46", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4006,9 +4006,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.44", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.44.tgz", - "integrity": "sha512-0EH3CQBCuuELiLBL1brc/o6Qx8CK723TJF5o68VXc60ha93Juo6LQ+dV+QgzFvVQ5RZTjBLtKB4MP8qw3YHCUQ==" + "version": "0.5.46", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.46.tgz", + "integrity": "sha512-bgxM8A+ccIydpTDRbISKmGt3XJb0rwX5cx04xGtqqhKX1Qs1OJM6NMGa3CKdqy6OiB7xXCPNLbi+KdqvJp9p9A==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 3cb5e8cc..c4e00181 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.44", + "warframe-public-export-plus": "^0.5.46", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index fac479f3..7d36984d 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -1,8 +1,123 @@ import { RequestHandler } from "express"; -import modularWeaponSale from "@/static/fixed_responses/modularWeaponSale.json"; +import { ExportWeapons } from "warframe-public-export-plus"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { CRng } from "@/src/services/rngService"; -const modularWeaponSaleController: RequestHandler = (_req, res) => { - res.json(modularWeaponSale); +// op=SyncAll +export const modularWeaponSaleController: RequestHandler = (_req, res) => { + const partTypeToParts: Record = {}; + for (const [uniqueName, data] of Object.entries(ExportWeapons)) { + if (data.partType) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + partTypeToParts[data.partType] ??= []; + partTypeToParts[data.partType].push(uniqueName); + } + } + + const today: number = Math.trunc(Date.now() / 86400000); + const kitgunIsPrimary: boolean = (today & 1) != 0; + res.json({ + SaleInfos: [ + getModularWeaponSale( + partTypeToParts, + today, + "Ostron", + ["LWPT_HILT", "LWPT_BLADE", "LWPT_HILT_WEIGHT"], + () => "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + ), + getModularWeaponSale( + partTypeToParts, + today, + "SolarisUnitedHoverboard", + ["LWPT_HB_DECK", "LWPT_HB_ENGINE", "LWPT_HB_FRONT", "LWPT_HB_JET"], + () => "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit" + ), + getModularWeaponSale( + partTypeToParts, + today, + "SolarisUnitedMoaPet", + ["LWPT_MOA_LEG", "LWPT_MOA_HEAD", "LWPT_MOA_ENGINE", "LWPT_MOA_PAYLOAD"], + () => "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit" + ), + getModularWeaponSale( + partTypeToParts, + today, + "SolarisUnitedKitGun", + [ + kitgunIsPrimary ? "LWPT_GUN_PRIMARY_HANDLE" : "LWPT_GUN_SECONDARY_HANDLE", + "LWPT_GUN_BARREL", + "LWPT_GUN_CLIP" + ], + (parts: string[]) => { + const barrel = parts[1]; + const gunType = ExportWeapons[barrel].gunType!; + if (kitgunIsPrimary) { + return { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" + }[gunType]; + } else { + return { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }[gunType]; + } + } + ) + ] + }); }; -export { modularWeaponSaleController }; +const priceFactor: Record = { + Ostron: 0.9, + SolarisUnitedHoverboard: 0.85, + SolarisUnitedMoaPet: 0.95, + SolarisUnitedKitGun: 0.9 +}; + +const getModularWeaponSale = ( + partTypeToParts: Record, + day: number, + name: string, + partTypes: string[], + getItemType: (parts: string[]) => string +): IModularWeaponSaleInfo => { + const rng = new CRng(day); + const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])); + let partsCost = 0; + for (const part of parts) { + const meta = ExportWeapons[part]; + if (!meta.premiumPrice) { + throw new Error(`no premium price for ${part}`); + } + partsCost += meta.premiumPrice; + } + return { + Name: name, + Expiry: toMongoDate(new Date((day + 1) * 86400000)), + Revision: day, + Weapons: [ + { + ItemType: getItemType(parts), + PremiumPrice: Math.trunc(partsCost * priceFactor[name]), + ModularParts: parts + } + ] + }; +}; + +interface IModularWeaponSaleInfo { + Name: string; + Expiry: IMongoDate; + Revision: number; + Weapons: IModularWeaponSaleItem[]; +} + +interface IModularWeaponSaleItem { + ItemType: string; + PremiumPrice: number; + ModularParts: string[]; +} diff --git a/src/services/rngService.ts b/src/services/rngService.ts index e20e5ce4..3a119e6f 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -69,3 +69,26 @@ export const getRandomWeightedRewardUc = ( } return getRandomReward(resultPool); }; + +export class CRng { + state: number; + + constructor(seed: number = 1) { + this.state = seed; + } + + random(): number { + this.state = (this.state * 1103515245 + 12345) & 0x7fffffff; + return (this.state & 0x3fffffff) / 0x3fffffff; + } + + randomInt(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(this.random() * (max - min + 1)) + min; + } + + randomElement(arr: T[]): T { + return arr[Math.floor(this.random() * arr.length)]; + } +} diff --git a/static/fixed_responses/modularWeaponSale.json b/static/fixed_responses/modularWeaponSale.json deleted file mode 100644 index 6d5bd172..00000000 --- a/static/fixed_responses/modularWeaponSale.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "SaleInfos": [ - { - "Name": "Ostron", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 3453, - "Weapons": [ - { - "ItemType": "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", - "PremiumPrice": 162, - "ModularParts": [ - "/Lotus/Weapons/Ostron/Melee/ModularMelee01/Handle/HandleFive", - "/Lotus/Weapons/Ostron/Melee/ModularMelee01/Tip/TipFour", - "/Lotus/Weapons/Ostron/Melee/ModularMelee01/Balance/BalanceSpeedICritII" - ] - } - ] - }, - { - "Name": "SolarisUnitedHoverboard", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 2058, - "Weapons": [ - { - "ItemType": "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit", - "PremiumPrice": 51, - "ModularParts": [ - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardSolarisA/HoverboardSolarisADeck", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardCorpusA/HoverboardCorpusAEngine", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardSolarisA/HoverboardSolarisAFront", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardCorpusB/HoverboardCorpusBJet" - ] - } - ] - }, - { - "Name": "SolarisUnitedMoaPet", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 2058, - "Weapons": [ - { - "ItemType": "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", - "PremiumPrice": 180, - "ModularParts": [ - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetLegB", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetHeadPara", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetEngineArcotek", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetPayloadMunitron" - ] - } - ] - }, - { - "Name": "SolarisUnitedKitGun", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 2058, - "Weapons": [ - { - "ItemType": "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", - "PremiumPrice": 184, - "ModularParts": [ - "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Handle/SUModularSecondaryHandleCPart", - "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart", - "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Clip/SUModularStatIReloadIIClipPart" - ] - } - ] - } - ] -} From b7f05e851c39d28ab6388d3b37a3ed3fda41cba3 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 04:33:12 -0700 Subject: [PATCH 177/776] fix: handle high spoofed mastery rank plus noDailyStandingLimits (#1201) In case the spoofed mastery rank is so high that 999,999 is *less* than the assumed maximum value for daily affiliation bins, we'll just use that so that the bar is always (at least) 100% full. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1201 --- src/controllers/api/inventoryController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 77f7bb1c..040557c3 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -246,8 +246,9 @@ export const getInventoryResponse = async ( } if (config.noDailyStandingLimits) { + const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500); for (const key of allDailyAffiliationKeys) { - inventoryResponse[key] = 999_999; + inventoryResponse[key] = spoofedDailyAffiliation; } } From 651ab5f6f1c2578e25a97f74896fc84fcb4fafc2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 04:33:21 -0700 Subject: [PATCH 178/776] feat: death marks (#1205) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1205 --- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/missionInventoryUpdateService.ts | 19 +++++++++++++++++++ src/types/requestTypes.ts | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index b0a84f5f..86b9c2c8 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1400,7 +1400,7 @@ const inventorySchema = new Schema( //Discount Coupon PendingCoupon: pendingCouponSchema, //Like BossAladV,BossCaptainVor come for you on missions % chance - DeathMarks: [String], + DeathMarks: { type: [String], default: [] }, //Zanuka Harvestable: Boolean, //Grustag three diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index fb4a936e..17f94c84 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -294,6 +294,25 @@ export const addMissionInventoryUpdates = async ( inventory.SeasonChallengeHistory.push(...processedCompletions); break; } + case "DeathMarks": { + for (const deathMark of value) { + if (!inventory.DeathMarks.find(x => x == deathMark)) { + // It's a new death mark; we have to say the line. + await createMessage(inventory.accountOwnerId.toString(), [ + { + sub: "/Lotus/Language/G1Quests/DeathMarkTitle", + sndr: "/Lotus/Language/G1Quests/DeathMarkSender", + msg: "/Lotus/Language/G1Quests/DeathMarkMessage", + icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", + highPriority: true + } + ]); + // TODO: This type of inbox message seems to automatically delete itself. Figure out under which conditions. + } + } + inventory.DeathMarks = value; + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index c3777112..8d1026bf 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -107,6 +107,7 @@ export type IMissionInventoryUpdateRequest = { DropTable: string; DROP_MOD: number[]; }[]; + DeathMarks?: string[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From c3a9b42fa2443f83bc7a7050b8c371317d60bd5f Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 04:33:48 -0700 Subject: [PATCH 179/776] fix: update slots where addEquipment is used (#1207) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1207 --- src/controllers/api/focusController.ts | 5 +-- .../api/modularWeaponCraftingController.ts | 10 ++++-- src/services/inventoryService.ts | 32 +++++++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 3 +- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index 29c92bf2..d65e60aa 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -1,7 +1,7 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, addMiscItems, addEquipment } from "@/src/services/inventoryService"; -import { IMiscItem, TFocusPolarity, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/services/inventoryService"; +import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; @@ -105,6 +105,7 @@ export const focusController: RequestHandler = async (req, res) => { ]; const inventory = await getInventory(accountId); const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts); + occupySlot(inventory, InventorySlot.AMPS, false); await inventory.save(); res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); break; diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 1f041f12..37b400fb 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -7,9 +7,12 @@ import { updateCurrency, addEquipment, addMiscItems, - applyDefaultUpgrades + applyDefaultUpgrades, + occupySlot, + productCategoryToInventoryBin } from "@/src/services/inventoryService"; import { ExportWeapons } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; const modularWeaponTypes: Record = { "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", @@ -47,7 +50,10 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const configs = applyDefaultUpgrades(inventory, ExportWeapons[data.Parts[0]]?.defaultUpgrades); // Give weapon - const inventoryChanges = addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }); + const inventoryChanges: IInventoryChanges = { + ...addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }), + ...occupySlot(inventory, productCategoryToInventoryBin(category)!, false) + }; // Remove credits & parts const miscItemChanges = []; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 0408115c..9c087f89 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -172,6 +172,38 @@ export const getInventory = async ( return inventory; }; +export const productCategoryToInventoryBin = (productCategory: string): InventorySlot | undefined => { + switch (productCategory) { + case "Suits": + return InventorySlot.SUITS; + case "Pistols": + case "LongGuns": + case "Melee": + return InventorySlot.WEAPONS; + case "Sentinels": + case "SentinelWeapons": + case "KubrowPets": + case "MoaPets": + return InventorySlot.SENTINELS; + case "SpaceSuits": + case "Hoverboards": + return InventorySlot.SPACESUITS; + case "SpaceGuns": + case "SpaceMelee": + return InventorySlot.SPACEWEAPONS; + case "OperatorAmps": + return InventorySlot.AMPS; + case "CrewShipWeapons": + case "CrewShipWeaponSkins": + return InventorySlot.RJ_COMPONENT_AND_ARMAMENTS; + case "MechSuits": + return InventorySlot.MECHSUITS; + case "CrewMembers": + return InventorySlot.CREWMEMBERS; + } + return undefined; +}; + export const occupySlot = ( inventory: TInventoryDatabaseDocument, bin: InventorySlot, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 2aee2a67..4a06de67 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -459,7 +459,8 @@ export enum InventorySlot { PVE_LOADOUTS = "PveBonusLoadoutBin", SENTINELS = "SentinelBin", AMPS = "OperatorAmpBin", - RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin" + RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin", + CREWMEMBERS = "CrewMemberBin" } export interface ISlots { From 818e09d4af2d2ed7068f37f06c423d01f5e6a0d0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 08:16:11 -0700 Subject: [PATCH 180/776] fix: only track clan log dateTime once contributions are done (#1210) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1210 --- src/controllers/api/getGuildLogController.ts | 4 +-- src/controllers/api/guildTechController.ts | 8 ++---- .../api/startDojoRecipeController.ts | 1 - src/models/guildModel.ts | 26 +++++++++++++------ src/types/guildTypes.ts | 16 ++++++++---- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 47c94b63..67940fde 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -20,14 +20,14 @@ export const getGuildLogController: RequestHandler = async (req, res) => { }; guild.RoomChanges?.forEach(entry => { log.RoomChanges.push({ - dateTime: toMongoDate(entry.dateTime), + dateTime: toMongoDate(entry.dateTime ?? new Date()), entryType: entry.entryType, details: entry.details }); }); guild.TechChanges?.forEach(entry => { log.TechChanges.push({ - dateTime: toMongoDate(entry.dateTime), + dateTime: toMongoDate(entry.dateTime ?? new Date()), entryType: entry.entryType, details: entry.details }); diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index f9a2e9fb..fab5cf0b 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -216,10 +216,6 @@ export const guildTechController: RequestHandler = async (req, res) => { const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; project.State = 0; guild.ActiveDojoColorResearch = data.RecipeType; - const entry = guild.TechChanges?.find(x => x.details == data.RecipeType); - if (entry) { - entry.dateTime = new Date(); - } await guild.save(); res.end(); } else { @@ -252,11 +248,11 @@ const setTechLogState = ( if (entry.entryType == state) { return false; } - entry.dateTime = dateTime ?? new Date(); + entry.dateTime = dateTime; entry.entryType = state; } else { guild.TechChanges.push({ - dateTime: dateTime ?? new Date(), + dateTime: dateTime, entryType: state, details: type }); diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index 04131d65..d2865165 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -39,7 +39,6 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { guild.RoomChanges ??= []; guild.RoomChanges.push({ - dateTime: new Date(), entryType: 2, details: request.PlacedComponent.pf, componentId: componentId diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 23d28b36..30057c76 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -6,9 +6,10 @@ import { ILongMOTD, IGuildMemberDatabase, IGuildLogEntryNumber, - IGuildLogEntryString, IGuildRank, - IGuildLogRoomChange + IGuildLogRoomChange, + IGuildLogEntryRoster, + IGuildLogEntryContributable } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -108,7 +109,17 @@ const defaultRanks: IGuildRank[] = [ } ]; -const guildLogEntryStringSchema = new Schema( +const guildLogRoomChangeSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: String, + componentId: Types.ObjectId + }, + { _id: false } +); + +const guildLogEntryContributableSchema = new Schema( { dateTime: Date, entryType: Number, @@ -117,12 +128,11 @@ const guildLogEntryStringSchema = new Schema( { _id: false } ); -const guildLogRoomChangeSchema = new Schema( +const guildLogEntryRosterSchema = new Schema( { dateTime: Date, entryType: Number, - details: String, - componentId: Types.ObjectId + details: String }, { _id: false } ); @@ -161,8 +171,8 @@ const guildSchema = new Schema( CeremonyResetDate: Date, CeremonyEndo: Number, RoomChanges: { type: [guildLogRoomChangeSchema], default: undefined }, - TechChanges: { type: [guildLogEntryStringSchema], default: undefined }, - RosterActivity: { type: [guildLogEntryStringSchema], default: undefined }, + TechChanges: { type: [guildLogEntryContributableSchema], default: undefined }, + RosterActivity: { type: [guildLogEntryRosterSchema], default: undefined }, ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined } }, { id: false } diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index ee10656b..f563e281 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -50,8 +50,8 @@ export interface IGuildDatabase { CeremonyResetDate?: Date; RoomChanges?: IGuildLogRoomChange[]; - TechChanges?: IGuildLogEntryString[]; - RosterActivity?: IGuildLogEntryString[]; + TechChanges?: IGuildLogEntryContributable[]; + RosterActivity?: IGuildLogEntryRoster[]; ClassChanges?: IGuildLogEntryNumber[]; } @@ -190,16 +190,22 @@ export interface ITechProjectDatabase extends Omit Date: Sun, 16 Mar 2025 08:16:27 -0700 Subject: [PATCH 181/776] chore: use updateOne for simple inventory field setters (#1211) e.g. changing syndicate pledge now takes ~6 ms instead of ~84 ms. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1211 --- .../api/addFriendImageController.ts | 23 +++++++++++++------ .../api/setEquippedInstrumentController.ts | 15 ++++++++---- .../api/setSupportedSyndicateController.ts | 15 ++++++++---- src/types/requestTypes.ts | 4 ---- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/controllers/api/addFriendImageController.ts b/src/controllers/api/addFriendImageController.ts index 3772f7ef..5f224ad8 100644 --- a/src/controllers/api/addFriendImageController.ts +++ b/src/controllers/api/addFriendImageController.ts @@ -1,16 +1,25 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { IUpdateGlyphRequest } from "@/src/types/requestTypes"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; -const addFriendImageController: RequestHandler = async (req, res) => { +export const addFriendImageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const json = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); - inventory.ActiveAvatarImageType = json.AvatarImageType; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + ActiveAvatarImageType: json.AvatarImageType + } + ); + res.json({}); }; -export { addFriendImageController }; +interface IUpdateGlyphRequest { + AvatarImageType: string; + AvatarImage: string; +} diff --git a/src/controllers/api/setEquippedInstrumentController.ts b/src/controllers/api/setEquippedInstrumentController.ts index 5b4b9800..bb80a815 100644 --- a/src/controllers/api/setEquippedInstrumentController.ts +++ b/src/controllers/api/setEquippedInstrumentController.ts @@ -1,14 +1,21 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; export const setEquippedInstrumentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const body = getJSONfromString(String(req.body)); - inventory.EquippedInstrument = body.Instrument; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + EquippedInstrument: body.Instrument + } + ); + res.end(); }; diff --git a/src/controllers/api/setSupportedSyndicateController.ts b/src/controllers/api/setSupportedSyndicateController.ts index e22b659f..40ce4af3 100644 --- a/src/controllers/api/setSupportedSyndicateController.ts +++ b/src/controllers/api/setSupportedSyndicateController.ts @@ -1,11 +1,18 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; export const setSupportedSyndicateController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); - inventory.SupportedSyndicate = req.query.syndicate as string; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + SupportedSyndicate: req.query.syndicate as string + } + ); + res.end(); }; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 8d1026bf..4364b77b 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -132,10 +132,6 @@ export interface IRewardInfo { export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; -export interface IUpdateGlyphRequest { - AvatarImageType: string; - AvatarImage: string; -} export interface IUpgradesRequest { ItemCategory: TEquipmentKey; ItemId: IOid; From 1d23f2736f4d79fcc70a05c08026468a44f30bed Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 08:16:49 -0700 Subject: [PATCH 182/776] chore: use inventory projection for getGuild requests (#1212) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1212 --- src/controllers/api/getGuildController.ts | 2 +- src/controllers/api/getGuildLogController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 0b7c5a95..37a9ed26 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -7,7 +7,7 @@ import { createUniqueClanName, getGuildClient } from "@/src/services/guildServic const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { const guild = await Guild.findOne({ _id: inventory.GuildId }); if (guild) { diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 67940fde..a0386e76 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -7,7 +7,7 @@ import { RequestHandler } from "express"; export const getGuildLogController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { const guild = await Guild.findOne({ _id: inventory.GuildId }); if (guild) { From 943edf70654a6c425855a26a71437641c07b66f9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 16:41:39 +0100 Subject: [PATCH 183/776] chore: use updateOne for active focus way change --- src/controllers/api/focusController.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index d65e60aa..a6c1c59c 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -5,6 +5,7 @@ import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/t import { logger } from "@/src/utils/logger"; import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; export const focusController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -55,9 +56,16 @@ export const focusController: RequestHandler = async (req, res) => { } case FocusOperation.ActivateWay: { const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType; - const inventory = await getInventory(accountId); - inventory.FocusAbility = focusType; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + FocusAbility: focusType + } + ); + res.end(); break; } From 6d12d908775bcf3e4ba8f282d594dc9c3a4b28e1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 16 Mar 2025 08:46:02 -0700 Subject: [PATCH 184/776] chore: add indexes for various models (#1213) These are looked up by the owner account id and/or assumed to exist only once per account. No index was added for "Ships" as that does not match these critera. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1213 --- src/models/inboxModel.ts | 2 ++ src/models/inventoryModels/inventoryModel.ts | 2 ++ src/models/inventoryModels/loadoutModel.ts | 2 ++ src/models/personalRoomsModel.ts | 2 ++ src/models/statsModel.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index c451d18a..c7c5e563 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -138,4 +138,6 @@ messageSchema.set("toJSON", { } }); +messageSchema.index({ ownerId: 1 }); + export const Inbox = model("Inbox", messageSchema, "inbox"); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 86b9c2c8..d7588a48 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1443,6 +1443,8 @@ inventorySchema.set("toJSON", { } }); +inventorySchema.index({ accountOwnerId: 1 }, { unique: true }); + // type overwrites for subdocuments/subdocument arrays export type InventoryDocumentProps = { FlavourItems: Types.DocumentArray; diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts index e43d33d1..dfa90bef 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -78,6 +78,8 @@ loadoutSchema.set("toJSON", { } }); +loadoutSchema.index({ loadoutOwnerId: 1 }, { unique: true }); + //create database typefor ILoadoutConfig type loadoutDocumentProps = { NORMAL: Types.DocumentArray; diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index e54d1b1c..5addf282 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -152,6 +152,8 @@ export const personalRoomsSchema = new Schema({ TailorShop: { type: tailorShopSchema, default: tailorShopDefault } }); +personalRoomsSchema.index({ personalRoomsOwnerId: 1 }, { unique: true }); + export const PersonalRooms = model( "PersonalRooms", personalRoomsSchema diff --git a/src/models/statsModel.ts b/src/models/statsModel.ts index c4ec87c3..93cf9c1f 100644 --- a/src/models/statsModel.ts +++ b/src/models/statsModel.ts @@ -103,6 +103,8 @@ statsSchema.set("toJSON", { } }); +statsSchema.index({ accountOwnerId: 1 }, { unique: true }); + export const Stats = model("Stats", statsSchema); // eslint-disable-next-line @typescript-eslint/ban-types From 1d091e3c4c8157081903afcb0648e23430706507 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 05:10:28 -0700 Subject: [PATCH 185/776] chore: remove consumables, recipes, etc. from array when their ItemCount becomes 0 (#1216) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1216 --- src/services/inventoryService.ts | 79 +++++++--------------- src/types/inventoryTypes/inventoryTypes.ts | 19 ++---- 2 files changed, 30 insertions(+), 68 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9c087f89..2886bf00 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -8,7 +8,6 @@ import { HydratedDocument, Types } from "mongoose"; import { SlotNames, IInventoryChanges, IBinChanges, slotNames } from "@/src/types/purchaseTypes"; import { IChallengeProgress, - IConsumable, IFlavourItem, IMiscItem, IMission, @@ -378,7 +377,7 @@ export const addItem = async ( { ItemType: typeName, ItemCount: quantity - } satisfies IConsumable + } satisfies ITypeCount ]; addConsumables(inventory, consumablesChanges); return { @@ -1102,74 +1101,42 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: }); }; -export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[]): void => { - const { ShipDecorations } = inventory; +const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => { + for (const change of changes) { + if (change.ItemCount != 0) { + let itemIndex = arr.findIndex(x => x.ItemType === change.ItemType); + if (itemIndex == -1) { + itemIndex = arr.push({ ItemType: change.ItemType, ItemCount: 0 }) - 1; + } - itemsArray.forEach(({ ItemCount, ItemType }) => { - const itemIndex = ShipDecorations.findIndex(miscItem => miscItem.ItemType === ItemType); - - if (itemIndex !== -1) { - ShipDecorations[itemIndex].ItemCount += ItemCount; - } else { - ShipDecorations.push({ ItemCount, ItemType }); + arr[itemIndex].ItemCount += change.ItemCount; + if (arr[itemIndex].ItemCount == 0) { + arr.splice(itemIndex, 1); + } else if (arr[itemIndex].ItemCount <= 0) { + logger.warn(`account now owns a negative amount of ${change.ItemType}`); + } } - }); + } }; -export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[]): void => { - const { Consumables } = inventory; +export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.ShipDecorations, itemsArray); +}; - itemsArray.forEach(({ ItemCount, ItemType }) => { - const itemIndex = Consumables.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - Consumables[itemIndex].ItemCount += ItemCount; - } else { - Consumables.push({ ItemCount, ItemType }); - } - }); +export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.Consumables, itemsArray); }; export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { - const { CrewShipRawSalvage } = inventory; - - itemsArray.forEach(({ ItemCount, ItemType }) => { - const itemIndex = CrewShipRawSalvage.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - CrewShipRawSalvage[itemIndex].ItemCount += ItemCount; - } else { - CrewShipRawSalvage.push({ ItemCount, ItemType }); - } - }); + applyArrayChanges(inventory.CrewShipRawSalvage, itemsArray); }; export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { - const { CrewShipAmmo } = inventory; - - itemsArray.forEach(({ ItemCount, ItemType }) => { - const itemIndex = CrewShipAmmo.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - CrewShipAmmo[itemIndex].ItemCount += ItemCount; - } else { - CrewShipAmmo.push({ ItemCount, ItemType }); - } - }); + applyArrayChanges(inventory.CrewShipAmmo, itemsArray); }; export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { - const { Recipes } = inventory; - - itemsArray.forEach(({ ItemCount, ItemType }) => { - const itemIndex = Recipes.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - Recipes[itemIndex].ItemCount += ItemCount; - } else { - Recipes.push({ ItemCount, ItemType }); - } - }); + applyArrayChanges(inventory.Recipes, itemsArray); }; export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 4a06de67..869cd4e7 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -236,8 +236,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu FusionTreasures: IFusionTreasure[]; WebFlags: IWebFlags; CompletedAlerts: string[]; - Consumables: IConsumable[]; - LevelKeys: IConsumable[]; + Consumables: ITypeCount[]; + LevelKeys: ITypeCount[]; TauntHistory?: ITaunt[]; StoryModeChoice: string; PeriodicMissionCompletions: IPeriodicMissionCompletionDatabase[]; @@ -265,7 +265,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType: string; - ShipDecorations: IConsumable[]; + ShipDecorations: ITypeCount[]; DiscoveredMarkers: IDiscoveredMarker[]; CompletedJobs: ICompletedJob[]; FocusAbility?: string; @@ -293,7 +293,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Settings: ISettings; PersonalTechProjects: IPersonalTechProject[]; PlayerSkills: IPlayerSkills; - CrewShipAmmo: IConsumable[]; + CrewShipAmmo: ITypeCount[]; CrewShipSalvagedWeaponSkins: IUpgradeClient[]; CrewShipWeapons: ICrewShipWeaponClient[]; CrewShipSalvagedWeapons: IEquipmentClient[]; @@ -303,7 +303,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu SubscribedToEmailsPersonalized: number; InfestedFoundry?: IInfestedFoundryClient; BlessingCooldown?: IMongoDate; - CrewShipRawSalvage: IConsumable[]; + CrewShipRawSalvage: ITypeCount[]; CrewMembers: ICrewMember[]; LotusCustomization: ILotusCustomization; UseAdultOperatorLoadout?: boolean; @@ -417,11 +417,6 @@ export interface ICompletedJob { StageCompletions: number[]; } -export interface IConsumable { - ItemCount: number; - ItemType: string; -} - export interface ICrewMember { ItemType: string; NemesisFingerprint: number; @@ -891,7 +886,7 @@ export enum GettingSlotOrderInfo { } export interface IGiving { - RawUpgrades: IConsumable[]; + RawUpgrades: ITypeCount[]; _SlotOrderInfo: GivingSlotOrderInfo[]; } @@ -924,7 +919,7 @@ export interface IPersonalTechProject { State: number; ReqCredits: number; ItemType: string; - ReqItems: IConsumable[]; + ReqItems: ITypeCount[]; CompletionDate?: IMongoDate; ItemId: IOid; ProductCategory?: string; From 0be54dd7cea04c4a946b0582d0d3c5a7c48c7873 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 05:10:44 -0700 Subject: [PATCH 186/776] feat: purchase modular weapon from daily special (#1217) Closes #685 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1217 --- .../api/modularWeaponCraftingController.ts | 31 +-- .../api/modularWeaponSaleController.ts | 177 ++++++++++++------ src/helpers/modularWeaponHelper.ts | 19 ++ src/routes/api.ts | 1 + src/services/itemDataService.ts | 13 ++ 5 files changed, 162 insertions(+), 79 deletions(-) create mode 100644 src/helpers/modularWeaponHelper.ts diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 37b400fb..2b116a8d 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -1,7 +1,6 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { getInventory, updateCurrency, @@ -11,26 +10,9 @@ import { occupySlot, productCategoryToInventoryBin } from "@/src/services/inventoryService"; -import { ExportWeapons } from "warframe-public-export-plus"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; - -const modularWeaponTypes: Record = { - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols", - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": "Melee", - "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets" -}; +import { getDefaultUpgrades } from "@/src/services/itemDataService"; +import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; interface IModularCraftRequest { WeaponType: string; @@ -46,14 +28,15 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const category = modularWeaponTypes[data.WeaponType]; const inventory = await getInventory(accountId); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const configs = applyDefaultUpgrades(inventory, ExportWeapons[data.Parts[0]]?.defaultUpgrades); - - // Give weapon + const defaultUpgrades = getDefaultUpgrades(data.Parts); + const configs = applyDefaultUpgrades(inventory, defaultUpgrades); const inventoryChanges: IInventoryChanges = { ...addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }), ...occupySlot(inventory, productCategoryToInventoryBin(category)!, false) }; + if (defaultUpgrades) { + inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); + } // Remove credits & parts const miscItemChanges = []; diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index 7d36984d..46c4bec5 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -3,9 +3,22 @@ import { ExportWeapons } from "warframe-public-export-plus"; import { IMongoDate } from "@/src/types/commonTypes"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { CRng } from "@/src/services/rngService"; +import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { + addEquipment, + applyDefaultUpgrades, + getInventory, + occupySlot, + productCategoryToInventoryBin, + updateCurrency +} from "@/src/services/inventoryService"; +import { getDefaultUpgrades } from "@/src/services/itemDataService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; -// op=SyncAll -export const modularWeaponSaleController: RequestHandler = (_req, res) => { +export const modularWeaponSaleController: RequestHandler = async (req, res) => { const partTypeToParts: Record = {}; for (const [uniqueName, data] of Object.entries(ExportWeapons)) { if (data.partType) { @@ -15,60 +28,105 @@ export const modularWeaponSaleController: RequestHandler = (_req, res) => { } } - const today: number = Math.trunc(Date.now() / 86400000); - const kitgunIsPrimary: boolean = (today & 1) != 0; - res.json({ - SaleInfos: [ - getModularWeaponSale( - partTypeToParts, - today, - "Ostron", - ["LWPT_HILT", "LWPT_BLADE", "LWPT_HILT_WEIGHT"], - () => "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" - ), - getModularWeaponSale( - partTypeToParts, - today, - "SolarisUnitedHoverboard", - ["LWPT_HB_DECK", "LWPT_HB_ENGINE", "LWPT_HB_FRONT", "LWPT_HB_JET"], - () => "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit" - ), - getModularWeaponSale( - partTypeToParts, - today, - "SolarisUnitedMoaPet", - ["LWPT_MOA_LEG", "LWPT_MOA_HEAD", "LWPT_MOA_ENGINE", "LWPT_MOA_PAYLOAD"], - () => "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit" - ), - getModularWeaponSale( - partTypeToParts, - today, - "SolarisUnitedKitGun", - [ - kitgunIsPrimary ? "LWPT_GUN_PRIMARY_HANDLE" : "LWPT_GUN_SECONDARY_HANDLE", - "LWPT_GUN_BARREL", - "LWPT_GUN_CLIP" - ], - (parts: string[]) => { - const barrel = parts[1]; - const gunType = ExportWeapons[barrel].gunType!; - if (kitgunIsPrimary) { - return { - GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", - GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", - GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" - }[gunType]; - } else { - return { - GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", - GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", - GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" - }[gunType]; - } + if (req.query.op == "SyncAll") { + res.json({ + SaleInfos: getSaleInfos(partTypeToParts, Math.trunc(Date.now() / 86400000)) + }); + } else if (req.query.op == "Purchase") { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = getJSONfromString(String(req.body)); + const weaponInfo = getSaleInfos(partTypeToParts, payload.Revision).find(x => x.Name == payload.SaleName)! + .Weapons[payload.ItemIndex]; + const category = modularWeaponTypes[weaponInfo.ItemType]; + const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts); + const configs = applyDefaultUpgrades(inventory, defaultUpgrades); + const inventoryChanges: IInventoryChanges = { + ...addEquipment( + inventory, + category, + weaponInfo.ItemType, + weaponInfo.ModularParts, + {}, + { + Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED, + ItemName: payload.ItemName, + Configs: configs, + Polarity: [ + { + Slot: payload.PolarizeSlot, + Value: payload.PolarizeValue + } + ] } - ) - ] - }); + ), + ...occupySlot(inventory, productCategoryToInventoryBin(category)!, true), + ...updateCurrency(inventory, weaponInfo.PremiumPrice, true) + }; + if (defaultUpgrades) { + inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); + } + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); + } else { + throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`); + } +}; + +const getSaleInfos = (partTypeToParts: Record, day: number): IModularWeaponSaleInfo[] => { + const kitgunIsPrimary: boolean = (day & 1) != 0; + return [ + getModularWeaponSale( + partTypeToParts, + day, + "Ostron", + ["LWPT_HILT", "LWPT_BLADE", "LWPT_HILT_WEIGHT"], + () => "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + ), + getModularWeaponSale( + partTypeToParts, + day, + "SolarisUnitedHoverboard", + ["LWPT_HB_DECK", "LWPT_HB_ENGINE", "LWPT_HB_FRONT", "LWPT_HB_JET"], + () => "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit" + ), + getModularWeaponSale( + partTypeToParts, + day, + "SolarisUnitedMoaPet", + ["LWPT_MOA_LEG", "LWPT_MOA_HEAD", "LWPT_MOA_ENGINE", "LWPT_MOA_PAYLOAD"], + () => "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit" + ), + getModularWeaponSale( + partTypeToParts, + day, + "SolarisUnitedKitGun", + [ + kitgunIsPrimary ? "LWPT_GUN_PRIMARY_HANDLE" : "LWPT_GUN_SECONDARY_HANDLE", + "LWPT_GUN_BARREL", + "LWPT_GUN_CLIP" + ], + (parts: string[]) => { + const barrel = parts[1]; + const gunType = ExportWeapons[barrel].gunType!; + if (kitgunIsPrimary) { + return { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" + }[gunType]; + } else { + return { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }[gunType]; + } + } + ) + ]; }; const priceFactor: Record = { @@ -121,3 +179,12 @@ interface IModularWeaponSaleItem { PremiumPrice: number; ModularParts: string[]; } + +interface IModularWeaponPurchaseRequest { + SaleName: string; + ItemIndex: number; + Revision: number; + ItemName: string; + PolarizeSlot: number; + PolarizeValue: ArtifactPolarity; +} diff --git a/src/helpers/modularWeaponHelper.ts b/src/helpers/modularWeaponHelper.ts new file mode 100644 index 00000000..82610000 --- /dev/null +++ b/src/helpers/modularWeaponHelper.ts @@ -0,0 +1,19 @@ +import { TEquipmentKey } from "../types/inventoryTypes/inventoryTypes"; + +export const modularWeaponTypes: Record = { + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": "Melee", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps", + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets" +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 5390d13a..fdea25dc 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -201,6 +201,7 @@ apiRouter.post("/joinSession.php", joinSessionController); apiRouter.post("/login.php", loginController); apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); +apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.post("/nameWeapon.php", nameWeaponController); apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 416adc7f..b1a1a297 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -29,6 +29,7 @@ import { ExportSentinels, ExportWarframes, ExportWeapons, + IDefaultUpgrade, IInboxMessage, IMissionReward, IPowersuit, @@ -256,3 +257,15 @@ export const toStoreItem = (type: string): string => { export const fromStoreItem = (type: string): string => { return "/Lotus/" + type.substring("/Lotus/StoreItems/".length); }; + +export const getDefaultUpgrades = (parts: string[]): IDefaultUpgrade[] | undefined => { + const allDefaultUpgrades: IDefaultUpgrade[] = []; + for (const part of parts) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const defaultUpgrades = ExportWeapons[part]?.defaultUpgrades; + if (defaultUpgrades) { + allDefaultUpgrades.push(...defaultUpgrades); + } + } + return allDefaultUpgrades.length == 0 ? undefined : allDefaultUpgrades; +}; From 6f3f1fe5b95bbd6f257b67172649053c0932ee64 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 14:33:47 +0100 Subject: [PATCH 187/776] fix: syndicate mission oids being longer than 24 chars --- src/controllers/dynamic/worldStateController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index d995f5e8..4772751b 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -53,7 +53,7 @@ export const worldStateController: RequestHandler = (req, res) => { const bountyCycleStart = bountyCycle * 9000000; const bountyCycleEnd = bountyCycleStart + 9000000; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000029" }, + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, Tag: "ZarimanSyndicate", @@ -61,7 +61,7 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000004" }, + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, Tag: "EntratiLabSyndicate", @@ -69,7 +69,7 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000006" }, + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "HexSyndicate", From 3eb5c366df0bd79bbbc6937435571ea354203dad Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 18:05:27 +0100 Subject: [PATCH 188/776] fix(webui): ignore empty archon shard slots --- static/webui/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index 34d65389..f66d6c2a 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -542,7 +542,7 @@ function updateInventory() { const uniqueUpgrades = {}; (item.ArchonCrystalUpgrades ?? []).forEach(upgrade => { - if (upgrade) { + if (upgrade && upgrade.UpgradeType) { uniqueUpgrades[upgrade.UpgradeType] ??= 0; uniqueUpgrades[upgrade.UpgradeType] += 1; } From 7b866a2f71e706ffbf1a4063c0ba879e1360102e Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 10:06:25 -0700 Subject: [PATCH 189/776] fix(webui): unable to add relics (#1222) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1222 --- src/controllers/custom/getItemListsController.ts | 16 +++++++++------- static/webui/script.js | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index cfdde03d..af20ce13 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -192,14 +192,16 @@ const getItemListsController: RequestHandler = (req, response) => { }); } for (const [uniqueName, arcane] of Object.entries(ExportArcanes)) { - const mod: ListedItem = { - uniqueName, - name: getString(arcane.name, lang) - }; - if (arcane.isFrivolous) { - mod.badReason = "frivolous"; + if (uniqueName.substring(0, 18) != "/Lotus/Types/Game/") { + const mod: ListedItem = { + uniqueName, + name: getString(arcane.name, lang) + }; + if (arcane.isFrivolous) { + mod.badReason = "frivolous"; + } + res.mods.push(mod); } - res.mods.push(mod); } for (const [uniqueName, syndicate] of Object.entries(ExportSyndicates)) { res.Syndicates.push({ diff --git a/static/webui/script.js b/static/webui/script.js index f66d6c2a..a506d19d 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -230,7 +230,7 @@ function fetchItemList() { if (type == "Syndicates" && item.uniqueName.startsWith("RadioLegion")) { item.name += " (" + item.uniqueName + ")"; } - if (item.uniqueName.substr(0, 18) != "/Lotus/Types/Game/" && item.badReason != "notraw") { + if (item.badReason != "notraw") { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; From f2afa6bb55f4220dd8f745474d8156d12053947e Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 10:43:59 -0700 Subject: [PATCH 190/776] chore: add GuildAdvertisementVendorManifest (#1221) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1221 --- src/services/serversideVendorsService.ts | 2 + .../GuildAdvertisementVendorManifest.json | 71 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index b06f6ae7..cae48e6e 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -14,6 +14,7 @@ import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorIn import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; +import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json"; import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json"; import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; @@ -63,6 +64,7 @@ const vendorManifests: IVendorManifest[] = [ DuviriAcrithisVendorManifest, EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabVendorManifest, + GuildAdvertisementVendorManifest, HubsIronwakeDondaVendorManifest, HubsPerrinSequenceWeaponVendorManifest, HubsRailjackCrewMemberVendorManifest, diff --git a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json new file mode 100644 index 00000000..05681d38 --- /dev/null +++ b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json @@ -0,0 +1,71 @@ +{ + "VendorInfo": { + "_id": { "$oid": "61ba123467e5d37975aeeb03" }, + "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon", + "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 12, "ProductCategory": "MiscItems" }], + "RegularPrice": [1, 1], + "Bin": "BIN_4", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 79554843, + "Id": { "$oid": "67bbb592e1534511d6c1c1e2" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain", + "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 7, "ProductCategory": "MiscItems" }], + "RegularPrice": [1, 1], + "Bin": "BIN_3", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 2413820225, + "Id": { "$oid": "67bbb592e1534511d6c1c1e3" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm", + "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 3, "ProductCategory": "MiscItems" }], + "RegularPrice": [1, 1], + "Bin": "BIN_2", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 3262300883, + "Id": { "$oid": "67bbb592e1534511d6c1c1e4" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow", + "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 20, "ProductCategory": "MiscItems" }], + "RegularPrice": [1, 1], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 2797325750, + "Id": { "$oid": "67bbb592e1534511d6c1c1e5" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost", + "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 10, "ProductCategory": "MiscItems" }], + "RegularPrice": [1, 1], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 554932310, + "Id": { "$oid": "67bbb592e1534511d6c1c1e6" } + } + ], + "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA", + "RandomSeedType": "VRST_FLAVOUR_TEXT", + "Expiry": { "$date": { "$numberLong": "9999999000000" } } + } +} From b4da4575011c85e723beaf71c5d8f236e8d1ddc1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 12:23:17 -0700 Subject: [PATCH 191/776] feat: mastery rank up inbox message (#1206) Closes #1203 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1206 Co-authored-by: Sainan Co-committed-by: Sainan --- .../api/trainingResultController.ts | 20 +++++++++++++++++++ src/models/inboxModel.ts | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/trainingResultController.ts b/src/controllers/api/trainingResultController.ts index 022d6c10..a9bc196e 100644 --- a/src/controllers/api/trainingResultController.ts +++ b/src/controllers/api/trainingResultController.ts @@ -5,6 +5,7 @@ import { IMongoDate } from "@/src/types/commonTypes"; import { RequestHandler } from "express"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { createMessage } from "@/src/services/inboxService"; interface ITrainingResultsRequest { numLevelsGained: number; @@ -26,6 +27,25 @@ const trainingResultController: RequestHandler = async (req, res): Promise if (trainingResults.numLevelsGained == 1) { inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23); inventory.PlayerLevel += 1; + + await createMessage(accountId, [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + msg: "/Lotus/Language/Inbox/MasteryRewardMsg", + arg: [ + { + Key: "NEW_RANK", + Tag: inventory.PlayerLevel + } + ], + att: [ + `/Lotus/Types/Items/ShipDecos/MasteryTrophies/Rank${inventory.PlayerLevel.toString().padStart(2, "0")}Trophy` + ], + sub: "/Lotus/Language/Inbox/MasteryRewardTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + highPriority: true + } + ]); } const changedinventory = await inventory.save(); diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index c7c5e563..c3ad8add 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -40,7 +40,7 @@ export interface IMessage { export interface Arg { Key: string; - Tag: string; + Tag: string | number; } //types are wrong @@ -99,7 +99,7 @@ const messageSchema = new Schema( type: [ { Key: String, - Tag: String, + Tag: Schema.Types.Mixed, _id: false } ], From f78616980a8d696465755a1c633bfcb0bd1daca7 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 18 Mar 2025 01:45:08 -0700 Subject: [PATCH 192/776] feat: archon hunt rotation (#1220) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1220 --- .../dynamic/worldStateController.ts | 78 +++++++++++++++++++ .../worldState/worldState.json | 15 ---- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 4772751b..8871fb83 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -8,6 +8,8 @@ import { buildConfig } from "@/src/services/buildConfigService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; +import { CRng } from "@/src/services/rngService"; +import { ExportRegions } from "warframe-public-export-plus"; export const worldStateController: RequestHandler = (req, res) => { const worldState: IWorldState = { @@ -18,6 +20,7 @@ export const worldStateController: RequestHandler = (req, res) => { Time: Math.round(Date.now() / 1000), Goals: [], GlobalUpgrades: [], + LiteSorties: [], EndlessXpChoices: [], ...staticWorldState }; @@ -114,6 +117,67 @@ export const worldStateController: RequestHandler = (req, res) => { }); } + // Archon Hunt cycling every week + { + const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; + const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; + const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth + + const nodes: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.factionIndex !== undefined && + value.factionIndex < 2 && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 // Exclude MT_ASSASSINATION + ) { + nodes.push(key); + } + } + + const rng = new CRng(week); + const firstNodeIndex = rng.randomInt(0, nodes.length - 1); + const firstNode = nodes[firstNodeIndex]; + nodes.splice(firstNodeIndex, 1); + worldState.LiteSorties.push({ + _id: { + $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", + Seed: week, + Boss: boss, + Missions: [ + { + missionType: rng.randomElement([ + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_EXTERMINATION", + "MT_SABOTAGE", + "MT_RESCUE" + ]), + node: firstNode + }, + { + missionType: rng.randomElement([ + "MT_DEFENSE", + "MT_TERRITORY", + "MT_ARTIFACT", + "MT_EXCAVATE", + "MT_SURVIVAL" + ]), + node: rng.randomElement(nodes) + }, + { + missionType: "MT_ASSASSINATION", + node: showdownNode + } + ] + }); + } + // Circuit choices cycling every week worldState.EndlessXpChoices.push({ Category: "EXC_NORMAL", @@ -197,6 +261,7 @@ interface IWorldState { Goals: IGoal[]; SyndicateMissions: ISyndicateMission[]; GlobalUpgrades: IGlobalUpgrade[]; + LiteSorties: ILiteSortie[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; KnownCalendarSeasons: ICalendarSeason[]; @@ -250,6 +315,19 @@ interface INodeOverride { CustomNpcEncounters?: string; } +interface ILiteSortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; + Seed: number; + Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL" + Missions: { + missionType: string; + node: string; + }[]; +} + interface IEndlessXpChoice { Category: string; Choices: string[]; diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 047dfd86..9a93bcb8 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -79,21 +79,6 @@ "Twitter": true } ], - "LiteSorties": [ - { - "_id": { "$oid": "663819fd1cec9ebe9d83a06e" }, - "Activation": { "$date": { "$numberLong": "1714953600000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Reward": "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", - "Seed": 58034, - "Boss": "SORTIE_BOSS_NIRA", - "Missions": [ - { "missionType": "MT_MOBILE_DEFENSE", "node": "SolNode125" }, - { "missionType": "MT_SURVIVAL", "node": "SolNode74" }, - { "missionType": "MT_ASSASSINATION", "node": "SolNode53" } - ] - } - ], "SyndicateMissions": [ { "_id": { "$oid": "663a4fc5ba6f84724fa48049" }, From 3e460c572848382deca27e5649a40891d166d145 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 18 Mar 2025 01:45:16 -0700 Subject: [PATCH 193/776] chore: update RewardSeed in database after generating a new one (#1226) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1226 --- .../api/getNewRewardSeedController.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index bbb3f71b..dc4cec8e 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,7 +1,22 @@ +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; -export const getNewRewardSeedController: RequestHandler = (_req, res) => { - res.json({ rewardSeed: generateRewardSeed() }); +export const getNewRewardSeedController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + + const rewardSeed = generateRewardSeed(); + logger.debug(`generated new reward seed: ${rewardSeed}`); + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + RewardSeed: rewardSeed + } + ); + res.json({ rewardSeed: rewardSeed }); }; export function generateRewardSeed(): number { From 8728cf3abf25bdabb76da4d53bd9c56633fb8e2c Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 18 Mar 2025 10:00:04 +0100 Subject: [PATCH 194/776] fix(webui): add riven placeholder text --- static/webui/index.html | 2 +- static/webui/script.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/static/webui/index.html b/static/webui/index.html index 540378d9..bc5e9d49 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -373,7 +373,7 @@ - + diff --git a/static/webui/script.js b/static/webui/script.js index a506d19d..b46b8fee 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -118,6 +118,9 @@ function updateLocElements() { document.querySelectorAll("[data-loc]").forEach(elm => { elm.innerHTML = loc(elm.getAttribute("data-loc")); }); + document.querySelectorAll("[data-loc-placeholder]").forEach(elm => { + elm.placeholder = loc(elm.getAttribute("data-loc-placeholder")); + }); } function setActiveLanguage(lang) { From 2a703de0cba04497c0c3416b7e2eafa27021396b Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 18 Mar 2025 04:24:11 -0700 Subject: [PATCH 195/776] chore: replace instances of `new Date().getTime()` with `Date.now()` (#1229) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1229 --- src/controllers/api/infestedFoundryController.ts | 8 +++----- src/controllers/dynamic/worldStateController.ts | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index aa7c99f5..f32a445e 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -128,7 +128,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const miscItemChanges: IMiscItem[] = []; let totalPercentagePointsGained = 0; - const currentUnixSeconds = Math.trunc(new Date().getTime() / 1000); + const currentUnixSeconds = Math.trunc(Date.now() / 1000); for (const contribution of request.ResourceContributions) { const snack = ExportMisc.helminthSnacks[contribution.ItemType]; @@ -260,9 +260,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { inventory.InfestedFoundry!.ConsumedSuits ??= []; inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit); inventory.InfestedFoundry!.LastConsumedSuit = suit; - inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date( - new Date().getTime() + 24 * 60 * 60 * 1000 - ); + inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(Date.now() + 24 * 60 * 60 * 1000); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00); addRecipes(inventory, recipeChanges); freeUpSlot(inventory, InventorySlot.SUITS); @@ -310,7 +308,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); const suit = inventory.Suits.id(request.SuitId.$oid)!; - const upgradesExpiry = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000); + const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.DefensiveUpgrade = request.DefensiveUpgradeType; suit.UpgradesExpiry = upgradesExpiry; diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 8871fb83..7632bbe5 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -43,7 +43,7 @@ export const worldStateController: RequestHandler = (req, res) => { } const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 - const day = Math.trunc((new Date().getTime() - EPOCH) / 86400000); + const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc(day / 7); const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; @@ -52,7 +52,7 @@ export const worldStateController: RequestHandler = (req, res) => { worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - const bountyCycle = Math.trunc(new Date().getTime() / 9000000); + const bountyCycle = Math.trunc(Date.now() / 9000000); const bountyCycleStart = bountyCycle * 9000000; const bountyCycleEnd = bountyCycleStart + 9000000; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { @@ -222,7 +222,7 @@ export const worldStateController: RequestHandler = (req, res) => { worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4); // Sentient Anomaly cycling every 30 minutes - const halfHour = Math.trunc(new Date().getTime() / (unixTimesInMs.hour / 2)); + const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); const tmp = { cavabegin: "1690761600", PurchasePlatformLockEnabled: true, From c98d872d5223ce8a436756174d9c969417b210df Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 18 Mar 2025 04:24:22 -0700 Subject: [PATCH 196/776] chore: use projection for drones request when possible (#1231) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1231 --- src/controllers/api/dronesController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index 972f8f6b..97e0d478 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -12,8 +12,8 @@ import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-ex export const dronesController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); if ("GetActive" in req.query) { + const inventory = await getInventory(accountId, "Drones"); const activeDrones: IActiveDrone[] = []; for (const drone of inventory.Drones) { if (drone.DeployTime) { @@ -39,6 +39,7 @@ export const dronesController: RequestHandler = async (req, res) => { ActiveDrones: activeDrones }); } else if ("droneId" in req.query && "systemIndex" in req.query) { + const inventory = await getInventory(accountId, "Drones"); const drone = inventory.Drones.id(req.query.droneId as string)!; const droneMeta = ExportDrones[drone.ItemType]; drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date(); @@ -76,6 +77,7 @@ export const dronesController: RequestHandler = async (req, res) => { await inventory.save(); res.json({}); } else if ("collectDroneId" in req.query) { + const inventory = await getInventory(accountId); const drone = inventory.Drones.id(req.query.collectDroneId as string)!; if (new Date() >= drone.DamageTime!) { From 6eebf0aa84bc904d46a2f59b5b8074273352c8bf Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 19 Mar 2025 20:38:14 +0100 Subject: [PATCH 197/776] chore: update request handling for 38.5.0 --- src/app.ts | 9 +++++++++ src/controllers/api/getFriendsController.ts | 1 + src/routes/api.ts | 1 + 3 files changed, 11 insertions(+) diff --git a/src/app.ts b/src/app.ts index 6f9bc0b9..a17ac9b4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,15 @@ import { webuiRouter } from "@/src/routes/webui"; const app = express(); +app.use((req, _res, next) => { + // 38.5.0 introduced "ezip" for encrypted body blobs. + // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it. + if (req.headers["content-encoding"] == "ezip") { + req.headers["content-encoding"] = undefined; + } + next(); +}); + app.use(bodyParser.raw()); app.use(express.json({ limit: "4mb" })); app.use(bodyParser.text()); diff --git a/src/controllers/api/getFriendsController.ts b/src/controllers/api/getFriendsController.ts index 292e107c..1227f84d 100644 --- a/src/controllers/api/getFriendsController.ts +++ b/src/controllers/api/getFriendsController.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; +// POST with {} instead of GET as of 38.5.0 const getFriendsController = (_request: Request, response: Response): void => { response.writeHead(200, { //Connection: "keep-alive", diff --git a/src/routes/api.ts b/src/routes/api.ts index fdea25dc..53029766 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -186,6 +186,7 @@ apiRouter.post("/focus.php", focusController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController); apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); +apiRouter.post("/getFriends.php", getFriendsController); apiRouter.post("/getGuildDojo.php", getGuildDojoController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); apiRouter.post("/gildWeapon.php", gildWeaponController); From ae05172ad842aa6617c7b092ad52e1249d0aa75c Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 01:09:39 +0100 Subject: [PATCH 198/776] chore: update PE+ for 38.5.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9dacff87..5461fff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.46", + "warframe-public-export-plus": "^0.5.47", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4006,9 +4006,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.46", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.46.tgz", - "integrity": "sha512-bgxM8A+ccIydpTDRbISKmGt3XJb0rwX5cx04xGtqqhKX1Qs1OJM6NMGa3CKdqy6OiB7xXCPNLbi+KdqvJp9p9A==" + "version": "0.5.47", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.47.tgz", + "integrity": "sha512-ZJK3VT1PdSPwZlhIzUVBlydwK4DM0sOmeCiixVMgOM8XuOPJ8OHfQUoLKydtw5rxCsowzFPbx5b3KBke5C4akQ==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index c4e00181..2ae66715 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.46", + "warframe-public-export-plus": "^0.5.47", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" From 0e1973e246cdb049c4b8e6b2ff20f57db33ba388 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:36:09 -0700 Subject: [PATCH 199/776] feat: start nemesis (#1227) Closes #446 As discussed there, some support for 64-bit integers without precision loss had to be hacked in. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1227 --- package-lock.json | 7 + package.json | 1 + src/controllers/api/guildTechController.ts | 2 + .../api/infestedFoundryController.ts | 1 + src/controllers/api/nemesisController.ts | 152 ++++++++++++++++++ src/helpers/stringHelpers.ts | 4 +- src/index.ts | 15 ++ src/models/inventoryModels/inventoryModel.ts | 55 ++++++- src/routes/api.ts | 2 + src/services/rngService.ts | 19 +++ src/types/inventoryTypes/inventoryTypes.ts | 53 +++--- 11 files changed, 286 insertions(+), 25 deletions(-) create mode 100644 src/controllers/api/nemesisController.ts diff --git a/package-lock.json b/package-lock.json index 5461fff0..2fdbe6b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", + "json-with-bigint": "^3.2.1", "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", @@ -2346,6 +2347,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.1.tgz", + "integrity": "sha512-0f8RHpU1AwBFwIPmtm71W+cFxzlXdiBmzc3JqydsNDSKSAsr0Lso6KXRbz0h2LRwTIRiHAk/UaD+xaAN5f577w==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", diff --git a/package.json b/package.json index 2ae66715..3d0ade47 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", + "json-with-bigint": "^3.2.1", "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index fab5cf0b..e1e79e8f 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -23,6 +23,7 @@ import { config } from "@/src/services/configService"; import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; import { TGuildDatabaseDocument } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { logger } from "@/src/utils/logger"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -219,6 +220,7 @@ export const guildTechController: RequestHandler = async (req, res) => { await guild.save(); res.end(); } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unknown guildTech action: ${data.Action}`); } }; diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index f32a445e..0e3b4ed7 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -355,6 +355,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { } default: + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`); } }; diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts new file mode 100644 index 00000000..550b2771 --- /dev/null +++ b/src/controllers/api/nemesisController.ts @@ -0,0 +1,152 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { SRng } from "@/src/services/rngService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; +import { ExportRegions } from "warframe-public-export-plus"; + +export const nemesisController: RequestHandler = async (req, res) => { + if ((req.query.mode as string) == "s") { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards"); + const body = getJSONfromString(String(req.body)); + + const infNodes: IInfNode[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex == 2 && // earth + value.nodeType != 3 && // not hub + value.nodeType != 7 && // not junction + value.missionIndex && // must have a mission type and not assassination + value.missionIndex != 28 && // not open world + value.missionIndex != 32 && // not railjack + value.missionIndex != 41 && // not saya's visions + value.name.indexOf("Archwing") == -1 + ) { + //console.log(dict_en[value.name]); + infNodes.push({ Node: key, Influence: 1 }); + } + } + + let weapons: readonly string[]; + if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { + weapons = kuvaLichVersionSixWeapons; + } else if ( + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" || + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree" + ) { + weapons = corpusVersionThreeWeapons; + } else { + throw new Error(`unknown nemesis manifest: ${body.target.manifest}`); + } + + body.target.fp = BigInt(body.target.fp); + const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); + let weaponIdx = initialWeaponIdx; + do { + const weapon = weapons[weaponIdx]; + if (!body.target.DisallowedWeapons.find(x => x == weapon)) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + inventory.Nemesis = { + fp: body.target.fp, + manifest: body.target.manifest, + KillingSuit: body.target.KillingSuit, + killingDamageType: body.target.killingDamageType, + ShoulderHelmet: body.target.ShoulderHelmet, + WeaponIdx: weaponIdx, + AgentIdx: body.target.AgentIdx, + BirthNode: body.target.BirthNode, + Faction: body.target.Faction, + Rank: 0, + k: false, + Traded: false, + d: new Date(), + InfNodes: infNodes, + GuessHistory: [], + Hints: [], + HintProgress: 0, + Weakened: body.target.Weakened, + PrevOwners: 0, + HenchmenKilled: 0, + SecondInCommand: body.target.SecondInCommand + }; + inventory.NemesisAbandonedRewards = []; // unclear if we need to do this since the client also submits this with missionInventoryUpdate + await inventory.save(); + + res.json({ + target: inventory.toJSON().Nemesis + }); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`); + } +}; + +export interface INemesisStartRequest { + target: { + fp: number | bigint; + manifest: string; + KillingSuit: string; + killingDamageType: number; + ShoulderHelmet: string; + DisallowedWeapons: string[]; + WeaponIdx: number; + AgentIdx: number; + BirthNode: string; + Faction: string; + Rank: number; + k: boolean; + Traded: boolean; + d: IMongoDate; + InfNodes: []; + GuessHistory: []; + Hints: []; + HintProgress: number; + Weakened: boolean; + PrevOwners: number; + HenchmenKilled: number; + SecondInCommand: boolean; + }; +} + +const kuvaLichVersionSixWeapons = [ + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", + "/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba", + "/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher", + "/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon", + "/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr", + "/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek" +]; + +const corpusVersionThreeWeapons = [ + "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron", + "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol", + "/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion" +]; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index 6ab13851..aee4355c 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -1,6 +1,8 @@ +import { JSONParse } from "json-with-bigint"; + export const getJSONfromString = (str: string): T => { const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); - return JSON.parse(jsonSubstring) as T; + return JSONParse(jsonSubstring); }; export const getSubstringFromKeyword = (str: string, keyword: string): string => { diff --git a/src/index.ts b/src/index.ts index 8bf614ef..4f7fc939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,21 @@ import { config, validateConfig } from "./services/configService"; import { registerLogFileCreationListener } from "@/src/utils/logger"; import mongoose from "mongoose"; +// Patch JSON.stringify to work flawlessly with Bigints. Yeah, it's not pretty. +// TODO: Might wanna use json-with-bigint if/when possible. +{ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + (BigInt.prototype as any).toJSON = function (): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return "" + this.toString() + ""; + }; + const og_stringify = JSON.stringify; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (JSON as any).stringify = (obj: any): string => { + return og_stringify(obj).split(`"`).join(``).split(`"`).join(``); + }; +} + registerLogFileCreationListener(); validateConfig(); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index d7588a48..0f087d7e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -79,7 +79,10 @@ import { ICrewShipWeaponDatabase, IRecentVendorPurchaseDatabase, IVendorPurchaseHistoryEntryDatabase, - IVendorPurchaseHistoryEntryClient + IVendorPurchaseHistoryEntryClient, + INemesisDatabase, + INemesisClient, + IInfNode } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1058,6 +1061,54 @@ const libraryDailyTaskInfoSchema = new Schema( { _id: false } ); +const infNodeSchema = new Schema( + { + Node: String, + Influence: Number + }, + { _id: false } +); + +const nemesisSchema = new Schema( + { + fp: BigInt, + manifest: String, + KillingSuit: String, + killingDamageType: Number, + ShoulderHelmet: String, + WeaponIdx: Number, + AgentIdx: Number, + BirthNode: String, + Faction: String, + Rank: Number, + k: Boolean, + Traded: Boolean, + d: Date, + PrevOwners: Number, + SecondInCommand: Boolean, + Weakened: Boolean, + InfNodes: [infNodeSchema], + HenchmenKilled: Number, + HintProgress: Number, + Hints: [Number], + GuessHistory: [Number] + }, + { _id: false } +); + +nemesisSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as INemesisDatabase; + const client = obj as INemesisClient; + + client.d = toMongoDate(db.d); + + delete obj._id; + delete obj.__v; + } +}); + const alignmentSchema = new Schema( { Alignment: Number, @@ -1341,7 +1392,7 @@ const inventorySchema = new Schema( //CorpusLich or GrineerLich NemesisAbandonedRewards: { type: [String], default: [] }, - //CorpusLich\KuvaLich + Nemesis: nemesisSchema, NemesisHistory: [Schema.Types.Mixed], LastNemesisAllySpawnTime: Schema.Types.Mixed, diff --git a/src/routes/api.ts b/src/routes/api.ts index 53029766..b2ef011d 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -69,6 +69,7 @@ import { missionInventoryUpdateController } from "@/src/controllers/api/missionI import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController"; import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; +import { nemesisController } from "@/src/controllers/api/nemesisController"; import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; @@ -204,6 +205,7 @@ apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.post("/nameWeapon.php", nameWeaponController); +apiRouter.post("/nemesis.php", nemesisController); apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/projectionManager.php", projectionManagerController); diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 3a119e6f..9791b5c2 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -70,6 +70,7 @@ export const getRandomWeightedRewardUc = ( return getRandomReward(resultPool); }; +// Seeded RNG for internal usage. Based on recommendations in the ISO C standards. export class CRng { state: number; @@ -92,3 +93,21 @@ export class CRng { return arr[Math.floor(this.random() * arr.length)]; } } + +// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth. +export class SRng { + state: bigint; + + constructor(seed: bigint) { + this.state = seed; + } + + randomInt(min: number, max: number): number { + const diff = max - min; + if (diff != 0) { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + min += (Number(this.state >> 32n) & 0x3fffffff) % (diff + 1); + } + return min; + } +} diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 869cd4e7..397ee1cc 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -43,6 +43,7 @@ export interface IInventoryDatabase | "Drones" | "RecentVendorPurchases" | "NextRefill" + | "Nemesis" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -71,6 +72,7 @@ export interface IInventoryDatabase Drones: IDroneDatabase[]; RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; NextRefill?: Date; + Nemesis?: INemesisDatabase; } export interface IQuestKeyDatabase { @@ -288,7 +290,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu SeasonChallengeHistory: ISeasonChallenge[]; EquippedInstrument?: string; InvasionChainProgress: IInvasionChainProgress[]; - NemesisHistory: INemesisHistory[]; + Nemesis?: INemesisClient; + NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; Settings: ISettings; PersonalTechProjects: IPersonalTechProject[]; @@ -782,38 +785,44 @@ export interface IMission extends IMissionDatabase { RewardsCooldownTime?: IMongoDate; } -export interface INemesisHistory { - fp: number; - manifest: Manifest; +export interface INemesisBaseClient { + fp: bigint; + manifest: string; KillingSuit: string; killingDamageType: number; ShoulderHelmet: string; + WeaponIdx: number; AgentIdx: number; - BirthNode: BirthNode; + BirthNode: string; + Faction: string; Rank: number; k: boolean; + Traded: boolean; d: IMongoDate; - GuessHistory?: number[]; - currentGuess?: number; - Traded?: boolean; - PrevOwners?: number; - SecondInCommand?: boolean; - Faction?: string; - Weakened?: boolean; + PrevOwners: number; + SecondInCommand: boolean; + Weakened: boolean; } -export enum BirthNode { - SolNode181 = "SolNode181", - SolNode4 = "SolNode4", - SolNode70 = "SolNode70", - SolNode76 = "SolNode76" +export interface INemesisBaseDatabase extends Omit { + d: Date; } -export enum Manifest { - LotusTypesEnemiesCorpusLawyersLawyerManifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest", - LotusTypesGameNemesisKuvaLichKuvaLichManifest = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest", - LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionThree = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree", - LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo" +export interface INemesisClient extends INemesisBaseClient { + InfNodes: IInfNode[]; + HenchmenKilled: number; + HintProgress: number; + Hints: number[]; + GuessHistory: number[]; +} + +export interface INemesisDatabase extends Omit { + d: Date; +} + +export interface IInfNode { + Node: string; + Influence: number; } export interface IPendingCouponDatabase { From 3986dac8ef348adb806122d5beba34ae8e95a30a Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:36:17 -0700 Subject: [PATCH 200/776] fix: buying flawed mods on iron wake doesn't consume credits (#1228) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1228 --- src/services/purchaseService.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 9336f0bf..5b3656c4 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -175,13 +175,21 @@ export const handlePurchase = async ( if (purchaseRequest.PurchaseParams.SourceId! in ExportVendors) { const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!]; const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem); - if (offer && offer.itemPrices) { - handleItemPrices( - inventory, - offer.itemPrices, - purchaseRequest.PurchaseParams.Quantity, - purchaseResponse.InventoryChanges - ); + if (offer) { + if (offer.credits) { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.credits, false) + ); + } + if (offer.itemPrices) { + handleItemPrices( + inventory, + offer.itemPrices, + purchaseRequest.PurchaseParams.Quantity, + purchaseResponse.InventoryChanges + ); + } } } break; From 2334e76453afd5c223a3f2f6174b60752bbfacad Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:36:29 -0700 Subject: [PATCH 201/776] feat(webui): max rank all intrinsics (#1230) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1230 --- src/controllers/api/playerSkillsController.ts | 2 +- .../custom/unlockAllIntrinsicsController.ts | 19 +++++++++++++++++++ src/routes/custom.ts | 2 ++ static/webui/index.html | 8 +++++--- static/webui/script.js | 6 ++++++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 10 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/controllers/custom/unlockAllIntrinsicsController.ts diff --git a/src/controllers/api/playerSkillsController.ts b/src/controllers/api/playerSkillsController.ts index de7ab4c8..5c8b301a 100644 --- a/src/controllers/api/playerSkillsController.ts +++ b/src/controllers/api/playerSkillsController.ts @@ -6,7 +6,7 @@ import { RequestHandler } from "express"; export const playerSkillsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "PlayerSkills"); const request = getJSONfromString(String(req.body)); const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; diff --git a/src/controllers/custom/unlockAllIntrinsicsController.ts b/src/controllers/custom/unlockAllIntrinsicsController.ts new file mode 100644 index 00000000..cd48bdcc --- /dev/null +++ b/src/controllers/custom/unlockAllIntrinsicsController.ts @@ -0,0 +1,19 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const unlockAllIntrinsicsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "PlayerSkills"); + inventory.PlayerSkills.LPS_PILOTING = 10; + inventory.PlayerSkills.LPS_GUNNERY = 10; + inventory.PlayerSkills.LPS_TACTICAL = 10; + inventory.PlayerSkills.LPS_ENGINEERING = 10; + inventory.PlayerSkills.LPS_COMMAND = 10; + inventory.PlayerSkills.LPS_DRIFT_COMBAT = 10; + inventory.PlayerSkills.LPS_DRIFT_RIDING = 10; + inventory.PlayerSkills.LPS_DRIFT_OPPORTUNITY = 10; + inventory.PlayerSkills.LPS_DRIFT_ENDURANCE = 10; + await inventory.save(); + res.end(); +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 7f53ad3e..fa0f2225 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -8,6 +8,7 @@ import { deleteAccountController } from "@/src/controllers/custom/deleteAccountC import { getNameController } from "@/src/controllers/custom/getNameController"; import { renameAccountController } from "@/src/controllers/custom/renameAccountController"; import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController"; +import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController"; import { createAccountController } from "@/src/controllers/custom/createAccountController"; import { createMessageController } from "@/src/controllers/custom/createMessageController"; @@ -30,6 +31,7 @@ customRouter.get("/deleteAccount", deleteAccountController); customRouter.get("/getName", getNameController); customRouter.get("/renameAccount", renameAccountController); customRouter.get("/ircDropped", ircDroppedController); +customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController); customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); diff --git a/static/webui/index.html b/static/webui/index.html index bc5e9d49..b698eb04 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -562,8 +562,11 @@
-

- +
+ + + +
@@ -578,7 +581,6 @@ -
diff --git a/static/webui/script.js b/static/webui/script.js index b46b8fee..2df57c47 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1053,6 +1053,12 @@ function doHelminthUnlockAll() { }); } +function doIntrinsicsUnlockAll() { + revalidateAuthz(() => { + $.get("/custom/unlockAllIntrinsics?" + window.authz); + }); +} + function doAddAllMods() { let modsAll = new Set(); for (const child of document.getElementById("datalist-mods").children) { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index aff0d599..8976fb63 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -123,6 +123,7 @@ dict = { cheats_account: `Account`, cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`, cheats_helminthUnlockAll: `Helminth vollständig aufleveln`, + cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`, cheats_changeButton: `Ändern`, cheats_none: `Keines`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index b60ba241..9896e46a 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -122,6 +122,7 @@ dict = { cheats_account: `Account`, cheats_unlockAllFocusSchools: `Unlock All Focus Schools`, cheats_helminthUnlockAll: `Fully Level Up Helminth`, + cheats_intrinsicsUnlockAll: `Max Rank All Intrinsics`, cheats_changeSupportedSyndicate: `Supported syndicate`, cheats_changeButton: `Change`, cheats_none: `None`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 96b52eaa..52b4537a 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -123,6 +123,7 @@ dict = { cheats_account: `Compte`, cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`, cheats_helminthUnlockAll: `Helminth niveau max`, + cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, cheats_changeSupportedSyndicate: `Allégeance`, cheats_changeButton: `Changer`, cheats_none: `Aucun`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 502b84af..0a50f913 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -123,6 +123,7 @@ dict = { cheats_account: `Аккаунт`, cheats_unlockAllFocusSchools: `Разблокировать все школы фокуса`, cheats_helminthUnlockAll: `Полностью улучшить Гельминта`, + cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`, cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 57207f0a..2f11b930 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -123,6 +123,7 @@ dict = { cheats_account: `账户`, cheats_unlockAllFocusSchools: `解锁所有专精学派`, cheats_helminthUnlockAll: `完全升级Helminth`, + cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, cheats_changeSupportedSyndicate: `支持的集团`, cheats_changeButton: `更改`, cheats_none: `无`, From 6135fdcdb97f095c7ae226556fe320c5f06a5b4e Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:36:36 -0700 Subject: [PATCH 202/776] fix: remove credits & ducats for purchases from baro (#1232) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1232 --- src/services/purchaseService.ts | 23 +++++++++++++++++++++++ src/types/purchaseTypes.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 5b3656c4..d2985f1d 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -134,6 +134,29 @@ export const handlePurchase = async ( }; switch (purchaseRequest.PurchaseParams.Source) { + case 1: { + if (purchaseRequest.PurchaseParams.SourceId! != worldState.VoidTraders[0]._id.$oid) { + throw new Error("invalid request source"); + } + const offer = worldState.VoidTraders[0].Manifest.find( + x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem + ); + if (offer) { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.RegularPrice, false) + ); + + const invItem: IMiscItem = { + ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks", + ItemCount: offer.PrimePrice * purchaseRequest.PurchaseParams.Quantity * -1 + }; + addMiscItems(inventory, [invItem]); + purchaseResponse.InventoryChanges.MiscItems ??= []; + purchaseResponse.InventoryChanges.MiscItems.push(invItem); + } + break; + } case 2: { const syndicateTag = purchaseRequest.PurchaseParams.SyndicateTag!; diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index d14f39f5..862bc095 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -14,7 +14,7 @@ export interface IPurchaseRequest { export interface IPurchaseParams { Source: number; - SourceId?: string; // for Source 7 & 18 + SourceId?: string; // for Source 1, 7 & 18 StoreItem: string; StorePage: string; SearchTerm: string; From 352c6df33969153428690eaf8839c62831009d46 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:36:45 -0700 Subject: [PATCH 203/776] fix: default PlacedDecos in schema to [] to match the type (#1235) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1235 --- src/models/personalRoomsModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index 5addf282..b8049d6f 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -57,7 +57,7 @@ const roomSchema = new Schema( { Name: String, MaxCapacity: Number, - PlacedDecos: { type: [placedDecosSchema], default: undefined } + PlacedDecos: { type: [placedDecosSchema], default: [] } }, { _id: false } ); From f0ebeab74e2e9ce0616326274a1a019f41b520bd Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:37:53 -0700 Subject: [PATCH 204/776] fix: when acquiring lich weapon, add innate damage (#1237) just fully randomised right now but better than adding these in a 'broken' state Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1237 --- src/services/inventoryService.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2886bf00..0b435240 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -389,13 +389,40 @@ export const addItem = async ( if (typeName in ExportWeapons) { const weapon = ExportWeapons[typeName]; if (weapon.totalDamage != 0) { + const defaultOverwrites: Partial = {}; + if (premiumPurchase) { + defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; + } + if ( + weapon.defaultUpgrades && + weapon.defaultUpgrades[0].ItemType == "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod" + ) { + defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod"; + defaultOverwrites.UpgradeFingerprint = JSON.stringify({ + compat: typeName, + buffs: [ + { + Tag: getRandomElement([ + "InnateElectricityDamage", + "InnateFreezeDamage", + "InnateHeatDamage", + "InnateImpactDamage", + "InnateMagDamage", + "InnateRadDamage", + "InnateToxinDamage" + ]), + Value: Math.trunc(Math.random() * 0x40000000) + } + ] + }); + } const inventoryChanges = addEquipment( inventory, weapon.productCategory, typeName, [], {}, - premiumPurchase ? { Features: EquipmentFeatures.DOUBLE_CAPACITY } : {} + defaultOverwrites ); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { From 88c5999d07814fee793da44cc5a29ab81bd6d7fe Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 05:50:48 -0700 Subject: [PATCH 205/776] chore: use SubdocumentArray.id in upgradesController (#1238) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1238 --- src/controllers/api/upgradesController.ts | 110 +++++++++------------- 1 file changed, 43 insertions(+), 67 deletions(-) diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index c29e20fa..5911ea6c 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -69,92 +69,68 @@ export const upgradesController: RequestHandler = async (req, res) => { } else switch (operation.UpgradeRequirement) { case "/Lotus/Types/Items/MiscItems/OrokinReactor": - case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.DOUBLE_CAPACITY; - break; - } - } + case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.DOUBLE_CAPACITY; break; + } case "/Lotus/Types/Items/MiscItems/UtilityUnlocker": - case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.UTILITY_SLOT; - break; - } - } + case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.UTILITY_SLOT; break; - case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": + } + case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": { console.assert(payload.ItemCategory == "SpaceGuns"); - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED; - break; - } - } + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED; break; + } case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker": case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker": case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker": - case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.ARCANE_SLOT; - break; - } - } + case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.ARCANE_SLOT; break; + } case "/Lotus/Types/Items/MiscItems/Forma": case "/Lotus/Types/Items/MiscItems/FormaUmbra": case "/Lotus/Types/Items/MiscItems/FormaAura": - case "/Lotus/Types/Items/MiscItems/FormaStance": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.XP = 0; - setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); - item.Polarized ??= 0; - item.Polarized += 1; - break; - } - } + case "/Lotus/Types/Items/MiscItems/FormaStance": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.XP = 0; + setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); + item.Polarized ??= 0; + item.Polarized += 1; break; - case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.ModSlotPurchases ??= 0; - item.ModSlotPurchases += 1; - break; - } - } + } + case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.ModSlotPurchases ??= 0; + item.ModSlotPurchases += 1; break; - case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.CustomizationSlotPurchases ??= 0; - item.CustomizationSlotPurchases += 1; - break; - } - } + } + case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.CustomizationSlotPurchases ??= 0; + item.CustomizationSlotPurchases += 1; break; - case "": + } + case "": { console.assert(operation.OperationType == "UOT_SWAP_POLARITY"); - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - for (let i = 0; i != operation.PolarityRemap.length; ++i) { - if (operation.PolarityRemap[i].Slot != i) { - setSlotPolarity(item, i, operation.PolarityRemap[i].Value); - } - } - break; + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + for (let i = 0; i != operation.PolarityRemap.length; ++i) { + if (operation.PolarityRemap[i].Slot != i) { + setSlotPolarity(item, i, operation.PolarityRemap[i].Value); } } break; + } default: throw new Error("Unsupported upgrade: " + operation.UpgradeRequirement); } From 1b4aee0b903b083b443b3adc1edae0338e96917b Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Thu, 20 Mar 2025 07:23:16 -0700 Subject: [PATCH 206/776] chore(webui): update to German translation (#1242) Translated the new `cheats_intrinsicsUnlockAll` string. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1242 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 8976fb63..5fc61661 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -123,7 +123,7 @@ dict = { cheats_account: `Account`, cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`, cheats_helminthUnlockAll: `Helminth vollständig aufleveln`, - cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, + cheats_intrinsicsUnlockAll: `Alle Inhärenzen auf Max. Rang`, cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`, cheats_changeButton: `Ändern`, cheats_none: `Keines`, From 9150d036d7a2fd7f435fa704d9f67fea09e9b75e Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 09:50:22 -0700 Subject: [PATCH 207/776] feat: installation of valence adapter (#1240) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1240 --- src/controllers/api/upgradesController.ts | 6 ++++++ src/types/inventoryTypes/commonInventoryTypes.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index 5911ea6c..157385f9 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -98,6 +98,12 @@ export const upgradesController: RequestHandler = async (req, res) => { item.Features |= EquipmentFeatures.ARCANE_SLOT; break; } + case "/Lotus/Types/Items/MiscItems/ValenceAdapter": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.VALENCE_SWAP; + break; + } case "/Lotus/Types/Items/MiscItems/Forma": case "/Lotus/Types/Items/MiscItems/FormaUmbra": case "/Lotus/Types/Items/MiscItems/FormaAura": diff --git a/src/types/inventoryTypes/commonInventoryTypes.ts b/src/types/inventoryTypes/commonInventoryTypes.ts index 5f3aeac0..13987e80 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -106,7 +106,8 @@ export enum EquipmentFeatures { GRAVIMAG_INSTALLED = 4, GILDED = 8, ARCANE_SLOT = 32, - INCARNON_GENESIS = 512 + INCARNON_GENESIS = 512, + VALENCE_SWAP = 1024 } export interface IEquipmentDatabase { From 31ad97e2151583f8e2b140773ce211045ad4e913 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 09:50:33 -0700 Subject: [PATCH 208/776] feat: valence swap (#1244) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1244 --- src/controllers/api/valenceSwapController.ts | 37 ++++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 39 insertions(+) create mode 100644 src/controllers/api/valenceSwapController.ts diff --git a/src/controllers/api/valenceSwapController.ts b/src/controllers/api/valenceSwapController.ts new file mode 100644 index 00000000..6196c9f1 --- /dev/null +++ b/src/controllers/api/valenceSwapController.ts @@ -0,0 +1,37 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const valenceSwapController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const body = JSON.parse(String(req.body)) as IValenceSwapRequest; + const inventory = await getInventory(accountId, body.WeaponCategory); + const weapon = inventory[body.WeaponCategory].id(body.WeaponId.$oid)!; + + const upgradeFingerprint = JSON.parse(weapon.UpgradeFingerprint!) as IInnateDamageFingerprint; + upgradeFingerprint.buffs[0].Tag = body.NewValenceUpgradeTag; + weapon.UpgradeFingerprint = JSON.stringify(upgradeFingerprint); + + await inventory.save(); + res.json({ + InventoryChanges: { + [body.WeaponCategory]: [weapon.toJSON()] + } + }); +}; + +interface IValenceSwapRequest { + WeaponId: IOid; + WeaponCategory: TEquipmentKey; + NewValenceUpgradeTag: string; +} + +interface IInnateDamageFingerprint { + compat: string; + buffs: { + Tag: string; + Value: number; + }[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index b2ef011d..15a6d32e 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -113,6 +113,7 @@ import { updateSessionGetController, updateSessionPostController } from "@/src/c import { updateSongChallengeController } from "@/src/controllers/api/updateSongChallengeController"; import { updateThemeController } from "@/src/controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; +import { valenceSwapController } from "@/src/controllers/api/valenceSwapController"; const apiRouter = express.Router(); @@ -241,5 +242,6 @@ apiRouter.post("/updateSession.php", updateSessionPostController); apiRouter.post("/updateSongChallenge.php", updateSongChallengeController); apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/upgrades.php", upgradesController); +apiRouter.post("/valenceSwap.php", valenceSwapController); export { apiRouter }; From b761ff1bffdd67da3e0448f19520ba09c319230f Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 10:08:00 -0700 Subject: [PATCH 209/776] fix: tell client of PrimeTokens inventory change when buying from varzia (#1243) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1243 --- src/services/purchaseService.ts | 12 ++++++++---- src/types/purchaseTypes.ts | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index d2985f1d..ef7a97b7 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -50,7 +50,7 @@ export const handlePurchase = async ( ): Promise => { logger.debug("purchase request", purchaseRequest); - const inventoryChanges: IInventoryChanges = {}; + const prePurchaseInventoryChanges: IInventoryChanges = {}; if (purchaseRequest.PurchaseParams.Source == 7) { const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (manifest) { @@ -65,7 +65,7 @@ export const handlePurchase = async ( inventory, offer.ItemPrices, purchaseRequest.PurchaseParams.Quantity, - inventoryChanges + prePurchaseInventoryChanges ); } if (!config.noVendorPurchaseLimits) { @@ -94,7 +94,7 @@ export const handlePurchase = async ( Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) }); } - inventoryChanges.RecentVendorPurchases = [ + prePurchaseInventoryChanges.RecentVendorPurchases = [ { VendorType: manifest.VendorInfo.TypeName, PurchaseHistory: [ @@ -121,7 +121,7 @@ export const handlePurchase = async ( inventory, purchaseRequest.PurchaseParams.Quantity ); - combineInventoryChanges(purchaseResponse.InventoryChanges, inventoryChanges); + combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); const currencyChanges = updateCurrency( inventory, @@ -240,6 +240,10 @@ export const handlePurchase = async ( purchaseResponse.InventoryChanges.MiscItems.push(invItem); } else if (!config.infiniteRegalAya) { inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; + + purchaseResponse.InventoryChanges.PrimeTokens ??= 0; + purchaseResponse.InventoryChanges.PrimeTokens -= + offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; } } break; diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 862bc095..35bb2123 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -36,6 +36,7 @@ export type IInventoryChanges = { RegularCredits?: number; PremiumCredits?: number; PremiumCreditsFree?: number; + PrimeTokens?: number; InfestedFoundry?: IInfestedFoundryClient; Drones?: IDroneClient[]; MiscItems?: IMiscItem[]; From 9d90a3ca264beaf05866ed2eb860ab8fdf6259c2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 15:27:15 -0700 Subject: [PATCH 210/776] fix: handle creation of infested lich (#1252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit just setting the höllvania nodes and preventing the generation of a weapon index Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1252 --- src/controllers/api/nemesisController.ts | 106 +++++++++++++++-------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 550b2771..7e1d72c7 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -13,46 +13,74 @@ export const nemesisController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards"); const body = getJSONfromString(String(req.body)); - - const infNodes: IInfNode[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - value.systemIndex == 2 && // earth - value.nodeType != 3 && // not hub - value.nodeType != 7 && // not junction - value.missionIndex && // must have a mission type and not assassination - value.missionIndex != 28 && // not open world - value.missionIndex != 32 && // not railjack - value.missionIndex != 41 && // not saya's visions - value.name.indexOf("Archwing") == -1 - ) { - //console.log(dict_en[value.name]); - infNodes.push({ Node: key, Influence: 1 }); - } - } - - let weapons: readonly string[]; - if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { - weapons = kuvaLichVersionSixWeapons; - } else if ( - body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" || - body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree" - ) { - weapons = corpusVersionThreeWeapons; - } else { - throw new Error(`unknown nemesis manifest: ${body.target.manifest}`); - } - body.target.fp = BigInt(body.target.fp); - const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); - let weaponIdx = initialWeaponIdx; - do { - const weapon = weapons[weaponIdx]; - if (!body.target.DisallowedWeapons.find(x => x == weapon)) { - break; + + let infNodes: IInfNode[]; + let weaponIdx = -1; + if (body.target.Faction == "FC_INFESTATION") { + infNodes = [ + { + Node: "SolNode852", + Influence: 1 + }, + { + Node: "SolNode850", + Influence: 1 + }, + { + Node: "SolNode851", + Influence: 1 + }, + { + Node: "SolNode853", + Influence: 1 + }, + { + Node: "SolNode854", + Influence: 1 + } + ]; + } else { + infNodes = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex == 2 && // earth + value.nodeType != 3 && // not hub + value.nodeType != 7 && // not junction + value.missionIndex && // must have a mission type and not assassination + value.missionIndex != 28 && // not open world + value.missionIndex != 32 && // not railjack + value.missionIndex != 41 && // not saya's visions + value.name.indexOf("Archwing") == -1 + ) { + //console.log(dict_en[value.name]); + infNodes.push({ Node: key, Influence: 1 }); + } } - weaponIdx = (weaponIdx + 1) % weapons.length; - } while (weaponIdx != initialWeaponIdx); + + let weapons: readonly string[]; + if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { + weapons = kuvaLichVersionSixWeapons; + } else if ( + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" || + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree" + ) { + weapons = corpusVersionThreeWeapons; + } else { + throw new Error(`unknown nemesis manifest: ${body.target.manifest}`); + } + + const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); + weaponIdx = initialWeaponIdx; + do { + const weapon = weapons[weaponIdx]; + if (!body.target.DisallowedWeapons.find(x => x == weapon)) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + } + inventory.Nemesis = { fp: body.target.fp, manifest: body.target.manifest, @@ -111,6 +139,8 @@ export interface INemesisStartRequest { Weakened: boolean; PrevOwners: number; HenchmenKilled: number; + MissionCount?: number; // Added in 38.5.0 + LastEnc?: number; // Added in 38.5.0 SecondInCommand: boolean; }; } From 9b16dc2c6a51c820368119e7c2a30d663653a641 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 20 Mar 2025 15:27:37 -0700 Subject: [PATCH 211/776] feat: valence fusion (#1251) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1251 --- src/controllers/api/nemesisController.ts | 59 ++++++++++++++++++-- src/controllers/api/valenceSwapController.ts | 10 +--- src/helpers/rivenHelper.ts | 6 +- src/types/inventoryTypes/inventoryTypes.ts | 17 ++---- 4 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 7e1d72c7..2f604604 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,16 +1,56 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory } from "@/src/services/inventoryService"; +import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { SRng } from "@/src/services/rngService"; -import { IMongoDate } from "@/src/types/commonTypes"; -import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { + IInfNode, + IInnateDamageFingerprint, + InventorySlot, + TEquipmentKey +} from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { ExportRegions } from "warframe-public-export-plus"; export const nemesisController: RequestHandler = async (req, res) => { - if ((req.query.mode as string) == "s") { - const accountId = await getAccountIdForRequest(req); + const accountId = await getAccountIdForRequest(req); + if ((req.query.mode as string) == "f") { + const body = getJSONfromString(String(req.body)); + const inventory = await getInventory(accountId, body.Category + " WeaponBin"); + const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!; + const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!; + const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; + const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; + + // Upgrade destination damage type if desireed + if (body.UseSourceDmgType) { + destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag; + } + + // Upgrade destination damage value + const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); + const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); + let newDamage = Math.max(destDamage, sourceDamage) * 1.1; + if (newDamage >= 0.58) { + newDamage = 0.6; + } + destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff); + + // Commit fingerprint + destWeapon.UpgradeFingerprint = JSON.stringify(destFingerprint); + + // Remove source weapon + inventory[body.Category].pull({ _id: body.SourceWeapon.$oid }); + freeUpSlot(inventory, InventorySlot.WEAPONS); + + await inventory.save(); + res.json({ + InventoryChanges: { + [body.Category]: [destWeapon.toJSON()] + } + }); + } else if ((req.query.mode as string) == "s") { const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards"); const body = getJSONfromString(String(req.body)); body.target.fp = BigInt(body.target.fp); @@ -116,7 +156,14 @@ export const nemesisController: RequestHandler = async (req, res) => { } }; -export interface INemesisStartRequest { +interface IValenceFusionRequest { + DestWeapon: IOid; + SourceWeapon: IOid; + Category: TEquipmentKey; + UseSourceDmgType: boolean; +} + +interface INemesisStartRequest { target: { fp: number | bigint; manifest: string; diff --git a/src/controllers/api/valenceSwapController.ts b/src/controllers/api/valenceSwapController.ts index 6196c9f1..0c3976b0 100644 --- a/src/controllers/api/valenceSwapController.ts +++ b/src/controllers/api/valenceSwapController.ts @@ -1,7 +1,7 @@ import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IOid } from "@/src/types/commonTypes"; -import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInnateDamageFingerprint, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; export const valenceSwapController: RequestHandler = async (req, res) => { @@ -27,11 +27,3 @@ interface IValenceSwapRequest { WeaponCategory: TEquipmentKey; NewValenceUpgradeTag: string; } - -interface IInnateDamageFingerprint { - compat: string; - buffs: { - Tag: string; - Value: number; - }[]; -} diff --git a/src/helpers/rivenHelper.ts b/src/helpers/rivenHelper.ts index e3819b64..35426a29 100644 --- a/src/helpers/rivenHelper.ts +++ b/src/helpers/rivenHelper.ts @@ -21,11 +21,11 @@ export interface IUnveiledRivenFingerprint { lvlReq: number; rerolls?: number; pol: string; - buffs: IRivenStat[]; - curses: IRivenStat[]; + buffs: IFingerprintStat[]; + curses: IFingerprintStat[]; } -interface IRivenStat { +export interface IFingerprintStat { Tag: string; Value: number; } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 397ee1cc..af7898d9 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -2,7 +2,6 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "../commonTypes"; import { - ArtifactPolarity, IColor, IItemConfig, IOperatorConfigClient, @@ -11,6 +10,7 @@ import { IEquipmentClient, IOperatorConfigDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper"; export type InventoryDatabaseEquipment = { [_ in TEquipmentKey]: IEquipmentDatabase[]; @@ -869,23 +869,14 @@ export interface IGetting { } export interface IRandomUpgrade { - UpgradeFingerprint: IUpgradeFingerprint; + UpgradeFingerprint: RivenFingerprint; ItemType: string; ItemId: IOid; } -export interface IUpgradeFingerprint { +export interface IInnateDamageFingerprint { compat: string; - lim: number; - lvlReq: number; - pol: ArtifactPolarity; - buffs: IBuff[]; - curses: IBuff[]; -} - -export interface IBuff { - Tag: string; - Value: number; + buffs: IFingerprintStat[]; } export enum GettingSlotOrderInfo { From 4cd35ef4d979e0cc41dfc7ab86b1a79a0d352a66 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Mar 2025 00:48:50 +0100 Subject: [PATCH 212/776] fix(webui): can't acquire entrati lanthorn --- src/controllers/custom/getItemListsController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index af20ce13..1c5a0cba 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -119,7 +119,10 @@ const getItemListsController: RequestHandler = (req, response) => { name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); } } - if (uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/") { + if ( + uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" && + uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle" + ) { res.miscitems.push({ uniqueName: uniqueName, name: name From 7d3f2e8796672e69812c16954cfeff2e0de0173a Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 21 Mar 2025 02:40:04 -0700 Subject: [PATCH 213/776] feat(stats): minigame stats (#1249) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1249 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/models/statsModel.ts | 7 ++++++- src/services/statsService.ts | 9 +++++++++ src/types/statTypes.ts | 10 ++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/models/statsModel.ts b/src/models/statsModel.ts index 93cf9c1f..64f31258 100644 --- a/src/models/statsModel.ts +++ b/src/models/statsModel.ts @@ -92,7 +92,12 @@ const statsSchema = new Schema({ Deaths: Number, HealCount: Number, ReviveCount: Number, - Races: { type: Map, of: raceSchema, default: {} } + Races: { type: Map, of: raceSchema, default: {} }, + ZephyrScore: Number, + SentinelGameScore: Number, + CaliberChicksScore: Number, + OlliesCrashCourseScore: Number, + DojoObstacleScore: Number }); statsSchema.set("toJSON", { diff --git a/src/services/statsService.ts b/src/services/statsService.ts index ebc5b2f2..00612247 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -305,6 +305,15 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: break; + case "ZephyrScore": + case "SentinelGameScore": + case "CaliberChicksScore": + case "OlliesCrashCourseScore": + case "DojoObstacleScore": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data; + break; + default: if (!ignoredCategories.includes(category)) { if (!unknownCategories[action]) { diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts index 6d3a2fd3..d020cff6 100644 --- a/src/types/statTypes.ts +++ b/src/types/statTypes.ts @@ -26,6 +26,11 @@ export interface IStatsClient { HealCount?: number; ReviveCount?: number; Races?: Map; + ZephyrScore?: number; + SentinelGameScore?: number; + CaliberChicksScore?: number; + OlliesCrashCourseScore?: number; + DojoObstacleScore?: number; } export interface IStatsDatabase extends IStatsClient { @@ -139,6 +144,11 @@ export interface IStatsMax { WEAPON_XP?: IUploadEntry; MISSION_SCORE?: IUploadEntry; RACE_SCORE?: IUploadEntry; + ZephyrScore?: number; + SentinelGameScore?: number; + CaliberChicksScore?: number; + OlliesCrashCourseScore?: number; + DojoObstacleScore?: number; } export interface IStatsSet { From 3c87dd56caaf909a881d328b10d829df23097961 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 21 Mar 2025 03:40:20 -0700 Subject: [PATCH 214/776] feat(stats): Ollie's Crash Course Rewards (#1260) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1260 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/stats/uploadController.ts | 5 ++--- src/services/statsService.ts | 26 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/controllers/stats/uploadController.ts b/src/controllers/stats/uploadController.ts index 89e5dfc3..c1c1d2e5 100644 --- a/src/controllers/stats/uploadController.ts +++ b/src/controllers/stats/uploadController.ts @@ -1,6 +1,6 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getStats, updateStats } from "@/src/services/statsService"; +import { updateStats } from "@/src/services/statsService"; import { IStatsUpdate } from "@/src/types/statTypes"; import { RequestHandler } from "express"; @@ -8,8 +8,7 @@ const uploadController: RequestHandler = async (req, res) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { PS, ...payload } = getJSONfromString(String(req.body)); const accountId = await getAccountIdForRequest(req); - const playerStats = await getStats(accountId); - await updateStats(playerStats, payload); + await updateStats(accountId, payload); res.status(200).end(); }; diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 00612247..79c667fb 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -9,7 +9,8 @@ import { IUploadEntry, IWeapon } from "@/src/types/statTypes"; -import { logger } from "../utils/logger"; +import { logger } from "@/src/utils/logger"; +import { addEmailItem, getInventory } from "@/src/services/inventoryService"; export const createStats = async (accountId: string): Promise => { const stats = new Stats({ accountOwnerId: accountId }); @@ -25,8 +26,9 @@ export const getStats = async (accountOwnerId: string): Promise => { +export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate): Promise => { const unknownCategories: Record = {}; + const playerStats = await getStats(accountOwnerId); for (const [action, actionData] of Object.entries(payload)) { switch (action) { @@ -308,12 +310,30 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "ZephyrScore": case "SentinelGameScore": case "CaliberChicksScore": - case "OlliesCrashCourseScore": case "DojoObstacleScore": playerStats[category] ??= 0; if (data > playerStats[category]) playerStats[category] = data; break; + case "OlliesCrashCourseScore": + playerStats[category] ??= 0; + if (!playerStats[category]) { + const inventory = await getInventory(accountOwnerId, "EmailItems"); + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/PlayedOlliesCrashCourseEmailItem" + ); + } + if (data >= 9991000 && playerStats[category] < 9991000) { + const inventory = await getInventory(accountOwnerId, "EmailItems"); + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/BeatOlliesCrashCourseInNinetySecEmailItem" + ); + } + if (data > playerStats[category]) playerStats[category] = data; + break; + default: if (!ignoredCategories.includes(category)) { if (!unknownCategories[action]) { From e83970d326facae1dafb1242a95605a4d2472bb2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Mar 2025 04:02:49 -0700 Subject: [PATCH 215/776] chore(stats): fix eslint warnings (#1262) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1262 --- src/services/statsService.ts | 22 +++++++++------------- src/types/statTypes.ts | 1 + 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 79c667fb..ef467723 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -148,7 +148,7 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) if (enemy) { if (category === "KILL_ENEMY") { enemy.kills ??= 0; - const captureCount = (actionData["CAPTURE_ENEMY"] as IUploadEntry)?.[type]; + const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type]; if (captureCount) { enemy.kills += Math.max(count - captureCount, 0); enemy.captures ??= 0; @@ -198,21 +198,19 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) break; case "CIPHER": - if (data["0"] > 0) { + if ((data as IUploadEntry)["0"] > 0) { playerStats.CiphersFailed ??= 0; - playerStats.CiphersFailed += data["0"]; + playerStats.CiphersFailed += (data as IUploadEntry)["0"]; } - if (data["1"] > 0) { + if ((data as IUploadEntry)["1"] > 0) { playerStats.CiphersSolved ??= 0; - playerStats.CiphersSolved += data["1"]; + playerStats.CiphersSolved += (data as IUploadEntry)["1"]; } break; default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; @@ -312,7 +310,7 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) case "CaliberChicksScore": case "DojoObstacleScore": playerStats[category] ??= 0; - if (data > playerStats[category]) playerStats[category] = data; + if (data > playerStats[category]) playerStats[category] = data as number; break; case "OlliesCrashCourseScore": @@ -331,14 +329,12 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) "/Lotus/Types/Items/EmailItems/BeatOlliesCrashCourseInNinetySecEmailItem" ); } - if (data > playerStats[category]) playerStats[category] = data; + if (data > playerStats[category]) playerStats[category] = data as number; break; default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts index d020cff6..9907c55d 100644 --- a/src/types/statTypes.ts +++ b/src/types/statTypes.ts @@ -134,6 +134,7 @@ export interface IStatsAdd { EXECUTE_ENEMY_ITEM?: IUploadEntry; KILL_ASSIST?: IUploadEntry; KILL_ASSIST_ITEM?: IUploadEntry; + CAPTURE_ENEMY?: IUploadEntry; } export interface IUploadEntry { From 6598318fc5cfb2283a5e441a25d7f8d03244e3e9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Mar 2025 05:19:42 -0700 Subject: [PATCH 216/776] feat: daily tribute (#1241) Closes #367 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1241 --- src/controllers/api/loginController.ts | 3 +- src/controllers/api/loginRewardsController.ts | 42 +++- .../api/loginRewardsSelectionController.ts | 57 ++++++ src/helpers/customHelpers/customHelpers.ts | 9 +- src/models/inventoryModels/inventoryModel.ts | 2 +- src/models/loginModel.ts | 4 +- src/routes/api.ts | 2 + src/services/loginRewardService.ts | 140 +++++++++++++ src/services/loginService.ts | 6 +- src/services/rngService.ts | 21 +- src/types/loginTypes.ts | 7 +- static/fixed_responses/loginRewards.json | 9 - .../loginRewards/randomRewards.json | 187 ++++++++++++++++++ 13 files changed, 460 insertions(+), 29 deletions(-) create mode 100644 src/controllers/api/loginRewardsSelectionController.ts create mode 100644 src/services/loginRewardService.ts delete mode 100644 static/fixed_responses/loginRewards.json create mode 100644 static/fixed_responses/loginRewards/randomRewards.json diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 0e7c53fb..9f70d0ad 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -42,8 +42,7 @@ export const loginController: RequestHandler = async (request, response) => { ForceLogoutVersion: 0, ConsentNeeded: false, TrackedSettings: [], - Nonce: nonce, - LatestEventMessageDate: new Date(0) + Nonce: nonce }); logger.debug("created new account"); response.json(createLoginResponse(myAddress, newAccount, buildLabel)); diff --git a/src/controllers/api/loginRewardsController.ts b/src/controllers/api/loginRewardsController.ts index fd4b40c4..f6430e28 100644 --- a/src/controllers/api/loginRewardsController.ts +++ b/src/controllers/api/loginRewardsController.ts @@ -1,8 +1,40 @@ import { RequestHandler } from "express"; -import loginRewards from "@/static/fixed_responses/loginRewards.json"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse } from "@/src/services/loginRewardService"; +import { getInventory } from "@/src/services/inventoryService"; -const loginRewardsController: RequestHandler = (_req, res) => { - res.json(loginRewards); +export const loginRewardsController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const today = Math.trunc(Date.now() / 86400000) * 86400; + + if (today == account.LastLoginRewardDate) { + res.end(); + return; + } + account.LoginDays += 1; + account.LastLoginRewardDate = today; + await account.save(); + + const inventory = await getInventory(account._id.toString()); + const randomRewards = getRandomLoginRewards(account, inventory); + const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; + const response: ILoginRewardsReponse = { + DailyTributeInfo: { + Rewards: randomRewards, + IsMilestoneDay: isMilestoneDay, + IsChooseRewardSet: randomRewards.length != 1, + LoginDays: account.LoginDays, + //NextMilestoneReward: "", + NextMilestoneDay: account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50, + HasChosenReward: false + }, + LastLoginRewardDate: today + }; + if (!isMilestoneDay && randomRewards.length == 1) { + response.DailyTributeInfo.HasChosenReward = true; + response.DailyTributeInfo.ChosenReward = randomRewards[0]; + response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); + await inventory.save(); + } + res.json(response); }; - -export { loginRewardsController }; diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts new file mode 100644 index 00000000..b1ebc9a6 --- /dev/null +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -0,0 +1,57 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const loginRewardsSelectionController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString()); + const body = JSON.parse(String(req.body)) as ILoginRewardsSelectionRequest; + const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; + if (body.IsMilestoneReward != isMilestoneDay) { + logger.warn(`Client disagrees on login milestone (got ${body.IsMilestoneReward}, expected ${isMilestoneDay})`); + } + let chosenReward; + let inventoryChanges: IInventoryChanges; + if (body.IsMilestoneReward) { + chosenReward = { + RewardType: "RT_STORE_ITEM", + StoreItemType: body.ChosenReward + }; + inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges; + if (!evergreenRewards.find(x => x == body.ChosenReward)) { + inventory.LoginMilestoneRewards.push(body.ChosenReward); + } + } else { + const randomRewards = getRandomLoginRewards(account, inventory); + chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; + inventoryChanges = await claimLoginReward(inventory, chosenReward); + } + await inventory.save(); + res.json({ + DailyTributeInfo: { + NewInventory: inventoryChanges, + ChosenReward: chosenReward + } + }); +}; + +interface ILoginRewardsSelectionRequest { + ChosenReward: string; + IsMilestoneReward: boolean; +} + +const evergreenRewards = [ + "/Lotus/Types/StoreItems/Packages/EvergreenTripleForma", + "/Lotus/Types/StoreItems/Packages/EvergreenTripleRifleRiven", + "/Lotus/Types/StoreItems/Packages/EvergreenTripleMeleeRiven", + "/Lotus/Types/StoreItems/Packages/EvergreenTripleSecondaryRiven", + "/Lotus/Types/StoreItems/Packages/EvergreenWeaponSlots", + "/Lotus/Types/StoreItems/Packages/EvergreenKuva", + "/Lotus/Types/StoreItems/Packages/EvergreenBoosters", + "/Lotus/Types/StoreItems/Packages/EvergreenEndo", + "/Lotus/Types/StoreItems/Packages/EvergreenExilus" +]; diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts index e1173d1f..a0163833 100644 --- a/src/helpers/customHelpers/customHelpers.ts +++ b/src/helpers/customHelpers/customHelpers.ts @@ -1,5 +1,5 @@ import { IAccountCreation } from "@/src/types/customTypes"; -import { IDatabaseAccount } from "@/src/types/loginTypes"; +import { IDatabaseAccountRequiredFields } from "@/src/types/loginTypes"; import crypto from "crypto"; import { isString, parseEmail, parseString } from "../general"; @@ -40,7 +40,7 @@ const toAccountCreation = (accountCreation: unknown): IAccountCreation => { throw new Error("incorrect account creation data: incorrect properties"); }; -const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => { +const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountRequiredFields => { return { ...createAccount, ClientType: "", @@ -48,9 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => CrossPlatformAllowed: true, ForceLogoutVersion: 0, TrackedSettings: [], - Nonce: 0, - LatestEventMessageDate: new Date(0) - } satisfies IDatabaseAccount; + Nonce: 0 + } satisfies IDatabaseAccountRequiredFields; }; export { toDatabaseAccount, toAccountCreation as toCreateAccount }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0f087d7e..238779f6 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1366,7 +1366,7 @@ const inventorySchema = new Schema( ThemeSounds: String, //Daily LoginRewards - LoginMilestoneRewards: [String], + LoginMilestoneRewards: { type: [String], default: [] }, //You first Dialog with NPC or use new Item NodeIntrosCompleted: [String], diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 47cbe8ff..2218a27d 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -21,7 +21,9 @@ const databaseAccountSchema = new Schema( TrackedSettings: { type: [String], default: [] }, Nonce: { type: Number, default: 0 }, Dropped: Boolean, - LatestEventMessageDate: { type: Date, default: 0 } + LatestEventMessageDate: { type: Date, default: 0 }, + LastLoginRewardDate: { type: Number, default: 0 }, + LoginDays: { type: Number, default: 0 } }, opts ); diff --git a/src/routes/api.ts b/src/routes/api.ts index 15a6d32e..0a1c238b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -63,6 +63,7 @@ import { inventorySlotsController } from "@/src/controllers/api/inventorySlotsCo import { joinSessionController } from "@/src/controllers/api/joinSessionController"; import { loginController } from "@/src/controllers/api/loginController"; import { loginRewardsController } from "@/src/controllers/api/loginRewardsController"; +import { loginRewardsSelectionController } from "@/src/controllers/api/loginRewardsSelectionController"; import { logoutController } from "@/src/controllers/api/logoutController"; import { marketRecommendationsController } from "@/src/controllers/api/marketRecommendationsController"; import { missionInventoryUpdateController } from "@/src/controllers/api/missionInventoryUpdateController"; @@ -202,6 +203,7 @@ apiRouter.post("/infestedFoundry.php", infestedFoundryController); apiRouter.post("/inventorySlots.php", inventorySlotsController); apiRouter.post("/joinSession.php", joinSessionController); apiRouter.post("/login.php", loginController); +apiRouter.post("/loginRewardsSelection.php", loginRewardsSelectionController); apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts new file mode 100644 index 00000000..4d5c562a --- /dev/null +++ b/src/services/loginRewardService.ts @@ -0,0 +1,140 @@ +import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json"; +import { IInventoryChanges } from "../types/purchaseTypes"; +import { TAccountDocument } from "./loginService"; +import { CRng, mixSeeds } from "./rngService"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { addBooster, updateCurrency } from "./inventoryService"; +import { handleStoreItemAcquisition } from "./purchaseService"; +import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus"; +import { toStoreItem } from "./itemDataService"; + +export interface ILoginRewardsReponse { + DailyTributeInfo: { + Rewards?: ILoginReward[]; // only set on first call of the day + IsMilestoneDay?: boolean; + IsChooseRewardSet?: boolean; + LoginDays?: number; // when calling multiple times per day, this is already incremented to represent "tomorrow" + //NextMilestoneReward?: ""; + NextMilestoneDay?: number; // seems to not be used if IsMilestoneDay + HasChosenReward?: boolean; + NewInventory?: IInventoryChanges; + ChosenReward?: ILoginReward; + }; + LastLoginRewardDate?: number; // only set on first call of the day; today at 0 UTC +} + +export interface ILoginReward { + //_id: IOid; + RewardType: string; + //CouponType: "CPT_PLATINUM"; + Icon: string; + //ItemType: ""; + StoreItemType: string; // uniquely identifies the reward + //ProductCategory: "Pistols"; + Amount: number; + ScalingMultiplier: number; + //Durability: "COMMON"; + //DisplayName: ""; + Duration: number; + //CouponSku: number; + //Rarity: number; + Transmission: string; +} + +const scaleAmount = (day: number, amount: number, scalingMultiplier: number): number => { + const divisor = 200 / (amount * scalingMultiplier); + return amount + Math.min(day, 3000) / divisor; +}; + +// Always produces the same result for the same account _id & LoginDays pair. +export const getRandomLoginRewards = ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument +): ILoginReward[] => { + const accountSeed = parseInt(account._id.toString().substring(16), 16); + const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); + const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)]; + // Using 25% an approximate chance for pick-a-doors. More conclusive data analysis is needed. + if (rng.random() < 0.25) { + do { + const reward = getRandomLoginReward(rng, account.LoginDays, inventory); + if (!rewards.find(x => x.StoreItemType == reward.StoreItemType)) { + rewards.push(reward); + } + } while (rewards.length != 3); + } + return rewards; +}; + +const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { + const reward = rng.randomReward(randomRewards)!; + //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; + if (reward.RewardType == "RT_RANDOM_RECIPE") { + // Not very faithful implementation but roughly the same idea + const masteredItems = new Set(); + for (const entry of inventory.XPInfo) { + masteredItems.add(entry.ItemType); + } + const unmasteredItems = new Set(); + for (const uniqueName of Object.keys(ExportWeapons)) { + if (!masteredItems.has(uniqueName)) { + unmasteredItems.add(uniqueName); + } + } + for (const uniqueName of Object.keys(ExportWarframes)) { + if (!masteredItems.has(uniqueName)) { + unmasteredItems.add(uniqueName); + } + } + const eligibleRecipes: string[] = []; + for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) { + if (unmasteredItems.has(recipe.resultType)) { + eligibleRecipes.push(uniqueName); + } + } + reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)); + } + return { + //_id: toOid(new Types.ObjectId()), + RewardType: reward.RewardType, + //CouponType: "CPT_PLATINUM", + Icon: reward.Icon ?? "", + //ItemType: "", + StoreItemType: reward.StoreItemType, + //ProductCategory: "Pistols", + Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)), + ScalingMultiplier: reward.ScalingMultiplier, + //Durability: "COMMON", + //DisplayName: "", + Duration: reward.Duration ? Math.round(reward.Duration * scaleAmount(day, 1, reward.ScalingMultiplier)) : 0, + //CouponSku: 0, + //Rarity: 0, + Transmission: reward.Transmission + }; +}; + +export const claimLoginReward = async ( + inventory: TInventoryDatabaseDocument, + reward: ILoginReward +): Promise => { + switch (reward.RewardType) { + case "RT_RESOURCE": + case "RT_STORE_ITEM": + case "RT_RECIPE": + case "RT_RANDOM_RECIPE": + return (await handleStoreItemAcquisition(reward.StoreItemType, inventory, reward.Amount)).InventoryChanges; + + case "RT_CREDITS": + return updateCurrency(inventory, -reward.Amount, false); + + case "RT_BOOSTER": { + const ItemType = ExportBoosters[reward.StoreItemType].typeName; + const ExpiryDate = 3600 * reward.Duration; + addBooster(ItemType, ExpiryDate, inventory); + return { + Boosters: [{ ItemType, ExpiryDate }] + }; + } + } + throw new Error(`unknown login reward type: ${reward.RewardType}`); +}; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 08d12ee9..099103be 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -1,6 +1,6 @@ import { Account } from "@/src/models/loginModel"; import { createInventory } from "@/src/services/inventoryService"; -import { IDatabaseAccount, IDatabaseAccountJson } from "@/src/types/loginTypes"; +import { IDatabaseAccountJson, IDatabaseAccountRequiredFields } from "@/src/types/loginTypes"; import { createShip } from "./shipService"; import { Document, Types } from "mongoose"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; @@ -18,7 +18,7 @@ export const isNameTaken = async (name: string): Promise => { return !!(await Account.findOne({ DisplayName: name })); }; -export const createAccount = async (accountData: IDatabaseAccount): Promise => { +export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise => { const account = new Account(accountData); try { await account.save(); @@ -62,7 +62,7 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ }; // eslint-disable-next-line @typescript-eslint/ban-types -type TAccountDocument = Document & +export type TAccountDocument = Document & IDatabaseAccountJson & { _id: Types.ObjectId; __v: number }; export const getAccountForRequest = async (req: Request): Promise => { diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 9791b5c2..cb8f2cba 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -18,11 +18,11 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -export const getRandomReward = (pool: T[]): T | undefined => { +const getRewardAtPercentage = (pool: T[], percentage: number): T | undefined => { if (pool.length == 0) return; const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); - const randomValue = Math.random() * totalChance; + const randomValue = percentage * totalChance; let cumulativeChance = 0; for (const item of pool) { @@ -34,6 +34,10 @@ export const getRandomReward = (pool: T[]): T throw new Error("What the fuck?"); }; +export const getRandomReward = (pool: T[]): T | undefined => { + return getRewardAtPercentage(pool, Math.random()); +}; + export const getRandomWeightedReward = ( pool: T[], weights: Record @@ -70,6 +74,15 @@ export const getRandomWeightedRewardUc = ( return getRandomReward(resultPool); }; +// ChatGPT generated this. It seems to have a good enough distribution. +export const mixSeeds = (seed1: number, seed2: number): number => { + let seed = seed1 ^ seed2; + seed ^= seed >>> 21; + seed ^= seed << 35; + seed ^= seed >>> 4; + return seed >>> 0; +}; + // Seeded RNG for internal usage. Based on recommendations in the ISO C standards. export class CRng { state: number; @@ -92,6 +105,10 @@ export class CRng { randomElement(arr: T[]): T { return arr[Math.floor(this.random() * arr.length)]; } + + randomReward(pool: T[]): T | undefined { + return getRewardAtPercentage(pool, this.random()); + } } // Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth. diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 0aaf2eed..8c443797 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -11,11 +11,16 @@ export interface IAccountAndLoginResponseCommons { Nonce: number; } -export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { +export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons { email: string; password: string; +} + +export interface IDatabaseAccount extends IDatabaseAccountRequiredFields { Dropped?: boolean; LatestEventMessageDate: Date; + LastLoginRewardDate: number; + LoginDays: number; } // Includes virtual ID diff --git a/static/fixed_responses/loginRewards.json b/static/fixed_responses/loginRewards.json deleted file mode 100644 index b6632556..00000000 --- a/static/fixed_responses/loginRewards.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "DailyTributeInfo": { - "IsMilestoneDay": false, - "IsChooseRewardSet": true, - "LoginDays": 1337, - "NextMilestoneReward": "", - "NextMilestoneDay": 50 - } -} diff --git a/static/fixed_responses/loginRewards/randomRewards.json b/static/fixed_responses/loginRewards/randomRewards.json new file mode 100644 index 00000000..20d3b60c --- /dev/null +++ b/static/fixed_responses/loginRewards/randomRewards.json @@ -0,0 +1,187 @@ +[ + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OxiumAlloy", + "Amount": 100, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Gallium", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/ChemFragment", + "Amount": 2, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Morphic", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/EnergyFragment", + "Amount": 2, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/NeuralSensor", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/BioFragment", + "Amount": 2, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Neurode", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCell", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Cryotic", + "Amount": 50, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Tellurium", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_CREDITS", + "StoreItemType": "", + "Icon": "/Lotus/Interface/Icons/StoreIcons/Currency/CreditsLarge.png", + "Amount": 10000, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/AffinityBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/CreditBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/ResourceAmountBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/ResourceDropChanceBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_STORE_ITEM", + "StoreItemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo" + }, + { + "RewardType": "RT_RECIPE", + "StoreItemType": "/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint", + "Amount": 1, + "ScalingMultiplier": 0.5, + "Rarity": "RARE", + "probability": 0.001467351430667816, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo" + }, + { + "RewardType": "RT_RANDOM_RECIPE", + "StoreItemType": "", + "Amount": 1, + "ScalingMultiplier": 0, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Teshin/DDayTribTeshin" + }, + { + "RewardType": "RT_STORE_ITEM", + "StoreItemType": "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "RARE", + "probability": 0.001467351430667816, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis" + } +] From 3b16ff9b548b07572e770a280d4e853a41858015 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Mar 2025 05:19:53 -0700 Subject: [PATCH 217/776] feat: getProfileViewingData for players (#1258) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1258 --- src/app.ts | 1 + src/controllers/api/inventoryController.ts | 2 +- .../getProfileViewingDataController.ts | 183 ++++++++++++++++++ src/models/guildModel.ts | 1 + src/models/inventoryModels/inventoryModel.ts | 1 - src/routes/dynamic.ts | 8 +- src/services/guildService.ts | 2 +- src/types/guildTypes.ts | 1 + src/types/inventoryTypes/inventoryTypes.ts | 1 - 9 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 src/controllers/dynamic/getProfileViewingDataController.ts diff --git a/src/app.ts b/src/app.ts index a17ac9b4..160a93c8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,6 +32,7 @@ app.use(requestLogger); app.use("/api", apiRouter); app.use("/", cacheRouter); app.use("/custom", customRouter); +app.use("/dynamic", dynamicController); app.use("/:id/dynamic", dynamicController); app.use("/pay", payRouter); app.use("/stats", statsRouter); diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 040557c3..36b1221d 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -297,7 +297,7 @@ const resourceGetParent = (resourceName: string): string | undefined => { }; // This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere. -const catBreadHash = (name: string): number => { +export const catBreadHash = (name: string): number => { let hash = 2166136261; for (let i = 0; i != name.length; ++i) { hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff; diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts new file mode 100644 index 00000000..9e9c764f --- /dev/null +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -0,0 +1,183 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; +import { Account } from "@/src/models/loginModel"; +import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; +import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { + IAffiliation, + IAlignment, + IChallengeProgress, + IDailyAffiliations, + ILoadoutConfigClient, + IMission, + IPlayerSkills, + ITypeXPItem +} from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; +import { catBreadHash } from "../api/inventoryController"; +import { ExportCustoms } from "warframe-public-export-plus"; + +export const getProfileViewingDataController: RequestHandler = async (req, res) => { + if (!req.query.playerId) { + res.status(400).end(); + return; + } + const account = await Account.findOne({ _id: req.query.playerId as string }, "DisplayName"); + if (!account) { + res.status(400).send("No account or guild ID specified"); + return; + } + const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; + const loadout = (await Loadout.findOne({ _id: inventory.LoadOutPresets }, "NORMAL"))!; + + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account._id), + DisplayName: account.DisplayName, + PlayerLevel: inventory.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory.XPInfo + }, + PlayerSkills: inventory.PlayerSkills, + ChallengeProgress: inventory.ChallengeProgress, + DeathMarks: inventory.DeathMarks, + Harvestable: inventory.Harvestable, + DeathSquadable: inventory.DeathSquadable, + Created: toMongoDate(inventory.Created), + MigratedToConsole: false, + Missions: inventory.Missions, + Affiliations: inventory.Affiliations, + DailyFocus: inventory.DailyFocus, + Wishlist: inventory.Wishlist, + Alignment: inventory.Alignment + }; + if (inventory.CurrentLoadOutIds.length) { + result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON(); + result.LoadOutPreset.ItemId = undefined; + const skins = new Set(); + if (result.LoadOutPreset.s) { + result.LoadOutInventory.Suits = [ + inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + } + if (result.LoadOutPreset.p) { + result.LoadOutInventory.Pistols = [ + inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + } + if (result.LoadOutPreset.l) { + result.LoadOutInventory.LongGuns = [ + inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); + } + if (result.LoadOutPreset.m) { + result.LoadOutInventory.Melee = [ + inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]); + } + for (const skin of skins) { + result.LoadOutInventory.WeaponSkins.push({ ItemType: skin }); + } + } + if (inventory.GuildId) { + const guild = (await Guild.findOne({ _id: inventory.GuildId }, "Name Tier XP Class"))!; + result.GuildId = toOid(inventory.GuildId); + result.GuildName = guild.Name; + result.GuildTier = guild.Tier; + result.GuildXp = guild.XP; + result.GuildClass = guild.Class; + result.GuildEmblem = false; + } + for (const key of allDailyAffiliationKeys) { + result[key] = inventory[key]; + } + + const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); + delete stats._id; + delete stats.__v; + delete stats.accountOwnerId; + + res.json({ + Results: [result], + TechProjects: [], + XpComponents: [], + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: stats + }); +}; + +interface IPlayerProfileViewingDataResult extends Partial { + AccountId: IOid; + DisplayName: string; + PlayerLevel: number; + LoadOutPreset?: Omit & { ItemId?: IOid }; + LoadOutInventory: { + WeaponSkins: { ItemType: string }[]; + Suits?: IEquipmentClient[]; + Pistols?: IEquipmentClient[]; + LongGuns?: IEquipmentClient[]; + Melee?: IEquipmentClient[]; + XPInfo: ITypeXPItem[]; + }; + GuildId?: IOid; + GuildName?: string; + GuildTier?: number; + GuildXp?: number; + GuildClass?: number; + GuildEmblem?: boolean; + PlayerSkills: IPlayerSkills; + ChallengeProgress: IChallengeProgress[]; + DeathMarks: string[]; + Harvestable: boolean; + DeathSquadable: boolean; + Created: IMongoDate; + MigratedToConsole: boolean; + Missions: IMission[]; + Affiliations: IAffiliation[]; + DailyFocus: number; + Wishlist: string[]; + Alignment?: IAlignment; +} + +let skinLookupTable: Record | undefined; + +const resolveAndCollectSkins = ( + inventory: TInventoryDatabaseDocument, + skins: Set, + item: IEquipmentClient +): void => { + for (const config of item.Configs) { + if (config.Skins) { + for (let i = 0; i != config.Skins.length; ++i) { + // Resolve oids to type names + if (config.Skins[i].length == 24) { + if (config.Skins[i].substring(0, 16) == "ca70ca70ca70ca70") { + if (!skinLookupTable) { + skinLookupTable = {}; + for (const key of Object.keys(ExportCustoms)) { + skinLookupTable[catBreadHash(key)] = key; + } + } + config.Skins[i] = skinLookupTable[parseInt(config.Skins[i].substring(16), 16)]; + } else { + const skinItem = inventory.WeaponSkins.id(config.Skins[i]); + config.Skins[i] = skinItem ? skinItem.ItemType : ""; + } + } + + // Collect type names + if (config.Skins[i]) { + skins.add(config.Skins[i]); + } + } + } + } +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 30057c76..d0f7c501 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -153,6 +153,7 @@ const guildSchema = new Schema( LongMOTD: { type: longMOTDSchema, default: undefined }, Ranks: { type: [guildRankSchema], default: defaultRanks }, TradeTax: { type: Number, default: 0 }, + Tier: { type: Number, default: 1 }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 238779f6..cdfc4dca 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -468,7 +468,6 @@ const seasonChallengeHistorySchema = new Schema( //TODO: check whether this is complete const playerSkillsSchema = new Schema( { - LPP_NONE: { type: Number, default: 0 }, LPP_SPACE: { type: Number, default: 0 }, LPS_PILOTING: { type: Number, default: 0 }, LPS_GUNNERY: { type: Number, default: 0 }, diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts index 0e808d48..14185e22 100644 --- a/src/routes/dynamic.ts +++ b/src/routes/dynamic.ts @@ -1,10 +1,12 @@ -import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; -import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; import express from "express"; +import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; +import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController"; +import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; const dynamicController = express.Router(); -dynamicController.get("/worldState.php", worldStateController); dynamicController.get("/aggregateSessions.php", aggregateSessionsController); +dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController); +dynamicController.get("/worldState.php", worldStateController); export { dynamicController }; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 9176e66b..5b00b622 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -91,7 +91,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s Members: members, Ranks: guild.Ranks, TradeTax: guild.TradeTax, - Tier: 1, + Tier: guild.Tier, Vault: getGuildVault(guild), ActiveDojoColorResearch: guild.ActiveDojoColorResearch, Class: guild.Class, diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index f563e281..7d10b57d 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -27,6 +27,7 @@ export interface IGuildDatabase { LongMOTD?: ILongMOTD; Ranks: IGuildRank[]; TradeTax: number; + Tier: number; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index af7898d9..fb0d3071 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -928,7 +928,6 @@ export interface IPersonalTechProject { } export interface IPlayerSkills { - LPP_NONE: number; LPP_SPACE: number; LPS_PILOTING: number; LPS_GUNNERY: number; From 5038095c13e95d20cc53b4d1f1fbf5c9442873e9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Mar 2025 05:20:01 -0700 Subject: [PATCH 218/776] fix(webui): hide unapplicable server settings elements (#1266) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1266 --- static/webui/script.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/webui/script.js b/static/webui/script.js index 2df57c47..544e768f 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -972,6 +972,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () { clearInterval(interval); fetch("/custom/config?" + window.authz).then(res => { if (res.status == 200) { + $("#server-settings-no-perms").addClass("d-none"); $("#server-settings").removeClass("d-none"); res.json().then(json => Object.entries(json).forEach(entry => { @@ -990,6 +991,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () { ); } else { $("#server-settings-no-perms").removeClass("d-none"); + $("#server-settings").addClass("d-none"); } }); } From aa95074ee0bad6779e96b6bab3fe08e8849b97a4 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 01:10:41 -0700 Subject: [PATCH 219/776] chore(webui): give feedback via toasts instead of alerts (#1269) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1269 --- static/webui/index.html | 1 + static/webui/script.js | 28 +++++++++++++++++++++++----- static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/static/webui/index.html b/static/webui/index.html index b698eb04..56e95ac8 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -593,6 +593,7 @@
+
diff --git a/static/webui/script.js b/static/webui/script.js index 544e768f..12894c2b 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -707,7 +707,7 @@ function maxRankAllEquipment(categories) { return sendBatchGearExp(batchData); } - alert(loc("code_noEquipmentToRankUp")); + toast(loc("code_noEquipmentToRankUp")); }); }); } @@ -740,6 +740,7 @@ function sendBatchGearExp(data) { contentType: "application/json", data: JSON.stringify(data) }).done(() => { + toast(loc("code_succRankUp")); updateInventory(); }); }); @@ -822,7 +823,7 @@ function doAcquireMiscItems() { } ]) }).done(function () { - alert(loc("code_succAdded")); + toast(loc("code_succAdded")); }); }); } @@ -1019,9 +1020,9 @@ function doUnlockAllFocusSchools() { await unlockFocusSchool(upgradeType); } if (Object.keys(missingFocusUpgrades).length == 0) { - alert(loc("code_focusAllUnlocked")); + toast(loc("code_focusAllUnlocked")); } else { - alert(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length)); + toast(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length)); } }); }); @@ -1155,7 +1156,7 @@ function doImport() { inventory: JSON.parse($("#import-inventory").val()) }) }).then(function () { - alert(loc("code_succImport")); + toast(loc("code_succImport")); updateInventory(); }); }); @@ -1192,3 +1193,20 @@ function doQuestUpdate(operation) { updateInventory(); }); } + +function toast(text) { + const toast = document.createElement("div"); + toast.className = "toast align-items-center text-bg-primary border-0"; + const div = document.createElement("div"); + div.className = "d-flex"; + const body = document.createElement("div"); + body.className = "toast-body"; + body.textContent = text; + div.appendChild(body); + const button = document.createElement("button"); + button.className = "btn-close btn-close-white me-2 m-auto"; + button.setAttribute("data-bs-dismiss", "toast"); + div.appendChild(button); + toast.appendChild(div); + new bootstrap.Toast(document.querySelector(".toast-container").appendChild(toast)).show(); +} diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 5fc61661..a3b4af35 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -26,6 +26,7 @@ dict = { code_renamePrompt: `Neuen benutzerdefinierten Namen eingeben:`, code_remove: `Entfernen`, code_addItemsConfirm: `Bist du sicher, dass du |COUNT| Gegenstände zu deinem Account hinzufügen möchtest?`, + code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `Keine Ausstattung zum Rangaufstieg verfügbar.`, code_succAdded: `Erfolgreich hinzugefügt.`, code_buffsNumber: `Anzahl der Buffs`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 9896e46a..4b10f0cf 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -25,6 +25,7 @@ dict = { code_renamePrompt: `Enter new custom name:`, code_remove: `Remove`, code_addItemsConfirm: `Are you sure you want to add |COUNT| items to your account?`, + code_succRankUp: `Successfully ranked up.`, code_noEquipmentToRankUp: `No equipment to rank up.`, code_succAdded: `Successfully added.`, code_buffsNumber: `Number of buffs`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 52b4537a..fc40232a 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -26,6 +26,7 @@ dict = { code_renamePrompt: `Nouveau nom :`, code_remove: `Retirer`, code_addItemsConfirm: `Ajouter |COUNT| items à l'inventaire ?`, + code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `No equipment to rank up.`, code_succAdded: `Ajouté.`, code_buffsNumber: `Nombre de buffs`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 0a50f913..e8148b85 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -26,6 +26,7 @@ dict = { code_renamePrompt: `Введите новое имя:`, code_remove: `Удалить`, code_addItemsConfirm: `Вы уверены, что хотите добавить |COUNT| предметов на ваш аккаунт?`, + code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`, code_succAdded: `Успешно добавлено.`, code_buffsNumber: `Количество усилений`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 2f11b930..f3dc7543 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -26,6 +26,7 @@ dict = { code_renamePrompt: `输入新的自定义名称:`, code_remove: `移除`, code_addItemsConfirm: `确定要向账户添加 |COUNT| 件物品吗?`, + code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `没有可升级的装备。`, code_succAdded: `已成功添加。`, code_buffsNumber: `增益数量`, From 42aca103ed9dddd0a57e4e4489bf23df30a1bd28 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 01:15:09 -0700 Subject: [PATCH 220/776] feat: handle ShipDecorations in missionInventoryUpdate (#1267) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1267 --- src/services/missionInventoryUpdateService.ts | 5 +++++ src/types/requestTypes.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 17f94c84..04171dc6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -24,6 +24,7 @@ import { addMissionComplete, addMods, addRecipes, + addShipDecorations, combineInventoryChanges, updateSyndicate } from "@/src/services/inventoryService"; @@ -147,6 +148,10 @@ export const addMissionInventoryUpdates = async ( case "CrewShipAmmo": addCrewShipAmmo(inventory, value); break; + case "ShipDecorations": + // e.g. when getting a 50+ score in happy zephyr, this is how the poster is given. + addShipDecorations(inventory, value); + break; case "FusionBundles": { let fusionPoints = 0; for (const fusionBundle of value) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 4364b77b..36a55bfb 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -47,6 +47,7 @@ export type IMissionInventoryUpdateRequest = { CrewShipAmmo?: ITypeCount[]; BonusMiscItems?: ITypeCount[]; EmailItems?: ITypeCount[]; + ShipDecorations?: ITypeCount[]; SyndicateId?: string; SortieId?: string; From 468ede680a420e662c351ecf368019fbb215db12 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 22 Mar 2025 03:36:55 -0700 Subject: [PATCH 221/776] chore(webui): update russain translations (#1273) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1273 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index e8148b85..766f0f0f 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -26,7 +26,7 @@ dict = { code_renamePrompt: `Введите новое имя:`, code_remove: `Удалить`, code_addItemsConfirm: `Вы уверены, что хотите добавить |COUNT| предметов на ваш аккаунт?`, - code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, + code_succRankUp: `Ранг успешно повышен`, code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`, code_succAdded: `Успешно добавлено.`, code_buffsNumber: `Количество усилений`, @@ -111,20 +111,20 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, - cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, - cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, + cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, - cheats_fastClanAscension: `[UNTRANSLATED] Fast Clan Ascension`, + cheats_fastClanAscension: `Мгновенное Вознесение Клана`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, cheats_saveSettings: `Сохранить настройки`, cheats_account: `Аккаунт`, cheats_unlockAllFocusSchools: `Разблокировать все школы фокуса`, cheats_helminthUnlockAll: `Полностью улучшить Гельминта`, - cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, + cheats_intrinsicsUnlockAll: `Полностью улучшить Модуляры`, cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`, cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, From beb02bffb01033a001eb55fe9ef801af4e458316 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 03:37:06 -0700 Subject: [PATCH 222/776] chore(webui): say "successfully removed" when using a negative quantity (#1271) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1271 --- static/webui/script.js | 31 +++++++++++++++++++------------ static/webui/translations/en.js | 1 + 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index 12894c2b..d786b2b4 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -812,20 +812,27 @@ function doAcquireMiscItems() { $("#miscitem-type").addClass("is-invalid").focus(); return; } - revalidateAuthz(() => { - $.post({ - url: "/custom/addItems?" + window.authz, - contentType: "application/json", - data: JSON.stringify([ - { - ItemType: uniqueName, - ItemCount: parseInt($("#miscitem-count").val()) + const count = parseInt($("#miscitem-count").val()); + if (count != 0) { + revalidateAuthz(() => { + $.post({ + url: "/custom/addItems?" + window.authz, + contentType: "application/json", + data: JSON.stringify([ + { + ItemType: uniqueName, + ItemCount: count + } + ]) + }).done(function () { + if (count > 0) { + toast(loc("code_succAdded")); + } else { + toast(loc("code_succRemoved")); } - ]) - }).done(function () { - toast(loc("code_succAdded")); + }); }); - }); + } } function doAcquireRiven() { diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 4b10f0cf..ee197c9e 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -28,6 +28,7 @@ dict = { code_succRankUp: `Successfully ranked up.`, code_noEquipmentToRankUp: `No equipment to rank up.`, code_succAdded: `Successfully added.`, + code_succRemoved: `Successfully removed.`, code_buffsNumber: `Number of buffs`, code_cursesNumber: `Number of curses`, code_rerollsNumber: `Number of rerolls`, From c6a2785175bb472215347bfbe6931a520f8984d5 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 22 Mar 2025 06:08:00 -0700 Subject: [PATCH 223/776] feat: clearing lich infuance (#1270) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1270 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/nemesisController.ts | 58 +++---------------- src/helpers/nemesisHelpers.ts | 32 ++++++++++ src/models/inventoryModels/inventoryModel.ts | 4 +- src/services/missionInventoryUpdateService.ts | 45 ++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 2 + src/types/purchaseTypes.ts | 2 + src/types/requestTypes.ts | 1 + 7 files changed, 92 insertions(+), 52 deletions(-) create mode 100644 src/helpers/nemesisHelpers.ts diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 2f604604..10c28c03 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,17 +1,12 @@ +import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { SRng } from "@/src/services/rngService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; -import { - IInfNode, - IInnateDamageFingerprint, - InventorySlot, - TEquipmentKey -} from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; -import { ExportRegions } from "warframe-public-export-plus"; export const nemesisController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -55,49 +50,8 @@ export const nemesisController: RequestHandler = async (req, res) => { const body = getJSONfromString(String(req.body)); body.target.fp = BigInt(body.target.fp); - let infNodes: IInfNode[]; let weaponIdx = -1; - if (body.target.Faction == "FC_INFESTATION") { - infNodes = [ - { - Node: "SolNode852", - Influence: 1 - }, - { - Node: "SolNode850", - Influence: 1 - }, - { - Node: "SolNode851", - Influence: 1 - }, - { - Node: "SolNode853", - Influence: 1 - }, - { - Node: "SolNode854", - Influence: 1 - } - ]; - } else { - infNodes = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - value.systemIndex == 2 && // earth - value.nodeType != 3 && // not hub - value.nodeType != 7 && // not junction - value.missionIndex && // must have a mission type and not assassination - value.missionIndex != 28 && // not open world - value.missionIndex != 32 && // not railjack - value.missionIndex != 41 && // not saya's visions - value.name.indexOf("Archwing") == -1 - ) { - //console.log(dict_en[value.name]); - infNodes.push({ Node: key, Influence: 1 }); - } - } - + if (body.target.Faction != "FC_INFESTATION") { let weapons: readonly string[]; if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { weapons = kuvaLichVersionSixWeapons; @@ -135,14 +89,16 @@ export const nemesisController: RequestHandler = async (req, res) => { k: false, Traded: false, d: new Date(), - InfNodes: infNodes, + InfNodes: getInfNodes(body.target.Faction, 0), GuessHistory: [], Hints: [], HintProgress: 0, Weakened: body.target.Weakened, PrevOwners: 0, HenchmenKilled: 0, - SecondInCommand: body.target.SecondInCommand + SecondInCommand: body.target.SecondInCommand, + MissionCount: 0, + LastEnc: 0 }; inventory.NemesisAbandonedRewards = []; // unclear if we need to do this since the client also submits this with missionInventoryUpdate await inventory.save(); diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts new file mode 100644 index 00000000..9fea8880 --- /dev/null +++ b/src/helpers/nemesisHelpers.ts @@ -0,0 +1,32 @@ +import { ExportRegions } from "warframe-public-export-plus"; +import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; + +export const getInfNodes = (faction: string, rank: number): IInfNode[] => { + const infNodes = []; + const systemIndex = systemIndexes[faction][rank]; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.nodeType != 3 && // not hub + value.nodeType != 7 && // not junction + value.missionIndex && // must have a mission type and not assassination + value.missionIndex != 28 && // not open world + value.missionIndex != 32 && // not railjack + value.missionIndex != 41 && // not saya's visions + value.missionIndex != 42 && // not face off + value.name.indexOf("1999NodeI") == -1 && // not stage defence + value.name.indexOf("1999NodeJ") == -1 && // not lich bounty + value.name.indexOf("Archwing") == -1 + ) { + //console.log(dict_en[value.name]); + infNodes.push({ Node: key, Influence: 1 }); + } + } + return infNodes; +}; + +const systemIndexes: Record = { + FC_GRINEER: [2, 3, 9, 11, 18], + FC_CORPUS: [1, 15, 4, 7, 8], + FC_INFESTATION: [23] +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index cdfc4dca..8e24b4c6 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1090,7 +1090,9 @@ const nemesisSchema = new Schema( HenchmenKilled: Number, HintProgress: Number, Hints: [Number], - GuessHistory: [Number] + GuessHistory: [Number], + MissionCount: Number, + LastEnc: Number }, { _id: false } ); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 04171dc6..44d0fdeb 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -42,6 +42,7 @@ import { createMessage } from "./inboxService"; import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json"; import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; +import { getInfNodes } from "@/src/helpers/nemesisHelpers"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -347,6 +348,7 @@ interface AddMissionRewardsReturnType { export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, { + Nemesis: nemesis, RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions, @@ -462,6 +464,49 @@ export const addMissionRewards = async ( } } + if (inventory.Nemesis) { + if ( + nemesis || + (inventory.Nemesis.Faction == "FC_INFESTATION" && + inventory.Nemesis.InfNodes.find(obj => obj.Node == rewardInfo.node)) + ) { + inventoryChanges.Nemesis ??= {}; + const nodeIndex = inventory.Nemesis.InfNodes.findIndex(obj => obj.Node === rewardInfo.node); + if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1); + + if (inventory.Nemesis.InfNodes.length <= 0) { + if (inventory.Nemesis.Faction != "FC_INFESTATION") { + inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4); + inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank; + } + inventory.Nemesis.InfNodes = getInfNodes(inventory.Nemesis.Faction, inventory.Nemesis.Rank); + } + + if (inventory.Nemesis.Faction == "FC_INFESTATION") { + inventoryChanges.Nemesis.HenchmenKilled ??= 0; + inventoryChanges.Nemesis.MissionCount ??= 0; + + inventory.Nemesis.HenchmenKilled += 5; + inventory.Nemesis.MissionCount += 1; + + inventoryChanges.Nemesis.HenchmenKilled += 5; + inventoryChanges.Nemesis.MissionCount += 1; + + if (inventory.Nemesis.HenchmenKilled >= 100) { + inventory.Nemesis.InfNodes = [ + { + Node: "CrewBattleNode559", + Influence: 1 + } + ]; + inventory.Nemesis.Weakened = true; + inventoryChanges.Nemesis.Weakened = true; + } + } + + inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; + } + } return { inventoryChanges, MissionRewards, credits }; }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index fb0d3071..59c1697a 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -814,6 +814,8 @@ export interface INemesisClient extends INemesisBaseClient { HintProgress: number; Hints: number[]; GuessHistory: number[]; + MissionCount: number; + LastEnc: number; } export interface INemesisDatabase extends Omit { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 35bb2123..c90e4588 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -3,6 +3,7 @@ import { IDroneClient, IInfestedFoundryClient, IMiscItem, + INemesisClient, ITypeCount, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; @@ -41,6 +42,7 @@ export type IInventoryChanges = { Drones?: IDroneClient[]; MiscItems?: IMiscItem[]; EmailItems?: ITypeCount[]; + Nemesis?: Partial; } & Record< Exclude< string, diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 36a55bfb..f9df3ff8 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -109,6 +109,7 @@ export type IMissionInventoryUpdateRequest = { DROP_MOD: number[]; }[]; DeathMarks?: string[]; + Nemesis?: number; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From 16bfcc44d512ad3ad8d2e711109b5d40ee4ecca2 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sat, 22 Mar 2025 06:44:05 -0700 Subject: [PATCH 224/776] chore(webui): update to translation files (#1278) - Update to German translation. - Added the new (previously missing) untranslated string to the other files. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1278 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 3 ++- static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index a3b4af35..4aa6d562 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -26,9 +26,10 @@ dict = { code_renamePrompt: `Neuen benutzerdefinierten Namen eingeben:`, code_remove: `Entfernen`, code_addItemsConfirm: `Bist du sicher, dass du |COUNT| Gegenstände zu deinem Account hinzufügen möchtest?`, - code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, + code_succRankUp: `Erfolgreich aufgestiegen.`, code_noEquipmentToRankUp: `Keine Ausstattung zum Rangaufstieg verfügbar.`, code_succAdded: `Erfolgreich hinzugefügt.`, + code_succRemoved: `Erfolgreich entfernt.`, code_buffsNumber: `Anzahl der Buffs`, code_cursesNumber: `Anzahl der Flüche`, code_rerollsNumber: `Anzahl der Umrollversuche`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index fc40232a..c51695bd 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -29,6 +29,7 @@ dict = { code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `No equipment to rank up.`, code_succAdded: `Ajouté.`, + code_succRemoved: `[UNTRANSLATED] Successfully removed.`, code_buffsNumber: `Nombre de buffs`, code_cursesNumber: `Nombre de débuffs`, code_rerollsNumber: `Nombre de rerolls`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 766f0f0f..5af34072 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -29,6 +29,7 @@ dict = { code_succRankUp: `Ранг успешно повышен`, code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`, code_succAdded: `Успешно добавлено.`, + code_succRemoved: `[UNTRANSLATED] Successfully removed.`, code_buffsNumber: `Количество усилений`, code_cursesNumber: `Количество проклятий`, code_rerollsNumber: `Количество циклов`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index f3dc7543..dbcf45d0 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -29,6 +29,7 @@ dict = { code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `没有可升级的装备。`, code_succAdded: `已成功添加。`, + code_succRemoved: `[UNTRANSLATED] Successfully removed.`, code_buffsNumber: `增益数量`, code_cursesNumber: `负面数量`, code_rerollsNumber: `洗卡次数`, From a0453ca61df35418164f5916a2a90b9d5ccaf0e4 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 07:30:16 -0700 Subject: [PATCH 225/776] feat: nemesis mode p (#1276) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1276 --- src/controllers/api/nemesisController.ts | 27 +++++++++++++++++++++++- src/helpers/nemesisHelpers.ts | 12 +++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 10c28c03..626105aa 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,4 +1,4 @@ -import { getInfNodes } from "@/src/helpers/nemesisHelpers"; +import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -45,6 +45,26 @@ export const nemesisController: RequestHandler = async (req, res) => { [body.Category]: [destWeapon.toJSON()] } }); + } else if ((req.query.mode as string) == "p") { + const inventory = await getInventory(accountId, "Nemesis"); + const body = getJSONfromString(String(req.body)); + const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction); + let guessResult = 0; + if (inventory.Nemesis!.Faction == "FC_INFESTATION") { + for (let i = 0; i != 3; ++i) { + if (body.guess[i] == passcode[0]) { + guessResult = 1 + i; + break; + } + } + } else { + for (let i = 0; i != 3; ++i) { + if (body.guess[i] == passcode[i]) { + ++guessResult; + } + } + } + res.json({ GuessResult: guessResult }); } else if ((req.query.mode as string) == "s") { const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards"); const body = getJSONfromString(String(req.body)); @@ -148,6 +168,11 @@ interface INemesisStartRequest { }; } +interface INemesisPrespawnCheckRequest { + guess: number[]; // .length == 3 + potency?: number[]; +} + const kuvaLichVersionSixWeapons = [ "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 9fea8880..839e0675 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -1,5 +1,6 @@ import { ExportRegions } from "warframe-public-export-plus"; import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; +import { SRng } from "@/src/services/rngService"; export const getInfNodes = (faction: string, rank: number): IInfNode[] => { const infNodes = []; @@ -30,3 +31,14 @@ const systemIndexes: Record = { FC_CORPUS: [1, 15, 4, 7, 8], FC_INFESTATION: [23] }; + +// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. +export const getNemesisPasscode = (fp: bigint, faction: string): number[] => { + const rng = new SRng(fp); + const passcode = [rng.randomInt(0, 7)]; + if (faction != "FC_INFESTATION") { + passcode.push(rng.randomInt(0, 7)); + passcode.push(rng.randomInt(0, 7)); + } + return passcode; +}; From 57786bfffb9c3f352a3d411f6a8d8c7725103526 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 07:30:29 -0700 Subject: [PATCH 226/776] fix: don't touch NemesisAbandonedRewards when spawning a lich (#1275) Because this can contain both grineer and corpus weapons, I think we should simply defer to the client's missionInventoryUpdate request in this matter. This still leaves open the possibility of the client crashing between spawning the lich and finishing the mission, but that's rather unlikely, I guess. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1275 --- src/controllers/api/nemesisController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 626105aa..b1f129db 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -66,7 +66,7 @@ export const nemesisController: RequestHandler = async (req, res) => { } res.json({ GuessResult: guessResult }); } else if ((req.query.mode as string) == "s") { - const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards"); + const inventory = await getInventory(accountId, "Nemesis"); const body = getJSONfromString(String(req.body)); body.target.fp = BigInt(body.target.fp); @@ -120,7 +120,6 @@ export const nemesisController: RequestHandler = async (req, res) => { MissionCount: 0, LastEnc: 0 }; - inventory.NemesisAbandonedRewards = []; // unclear if we need to do this since the client also submits this with missionInventoryUpdate await inventory.save(); res.json({ From b8e3be50185ce8907afb8bdf6a54b9b7abdea2cd Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 21:11:16 +0100 Subject: [PATCH 227/776] chore: add IOtherDialogueInfo --- src/controllers/api/saveDialogueController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 332cb235..55819636 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -81,5 +81,11 @@ interface SaveCompletedDialogueRequest { Booleans: string[]; ResetBooleans: string[]; Data: ICompletedDialogue; - OtherDialogueInfos: string[]; // unsure + OtherDialogueInfos: IOtherDialogueInfo[]; // unsure +} + +interface IOtherDialogueInfo { + Dialogue: string; + Tag: string; + Value: number; } From 4b3b551ba795543625bd55cddc47bd0a5548852d Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 17:35:18 -0700 Subject: [PATCH 228/776] fix: properly commit boosters to inventory (#1279) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1279 --- src/services/inventoryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 0b435240..e6bc21be 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1286,7 +1286,7 @@ export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, }; export const addBooster = (ItemType: string, time: number, inventory: TInventoryDatabaseDocument): void => { - const currentTime = Math.floor(Date.now() / 1000) - 129600; // Value is wrong without 129600. Figure out why, please. :) + const currentTime = Math.floor(Date.now() / 1000); const { Boosters } = inventory; From 7414658340520f3d7cc93284e1acedfc8698ea80 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 17:35:34 -0700 Subject: [PATCH 229/776] fix: add missing items from codex objects list to allScans (#1282) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1282 --- static/fixed_responses/allScans.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 2263ad95..716c1a3b 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -35,6 +35,7 @@ "/Lotus/Types/Items/Plants/WildGingerBPlant", "/Lotus/Objects/Guild/Props/Computers/PanelADeco", "/Lotus/Types/PickUps/LootContainers/CorpusLootCrateCommon", + "/Lotus/Objects/Gameplay/InfestedHiveMode/InfestedTumorObjectiveDeco", "/Lotus/Objects/Gameplay/InfestedHiveMode/InfestedTumorObjectiveSpawnedDeco", "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarF", "/Lotus/Types/Friendly/Pets/DecoyCatbrowPetAvatar", @@ -1088,5 +1089,8 @@ "/Lotus/Weapons/Infested/Melee/InfBoomerang/InfBoomerangSpawnAvatar", "/Lotus/Types/Game/CrewShip/GrineerDestroyer/DeepSpace/GrineerDSDestroyerAvatar", "/Lotus/Types/Game/CrewShip/GrineerDestroyer/Saturn/GrineerSaturnDestroyerAvatar", - "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerDestroyerAvatar" + "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerDestroyerAvatar", + "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateUltraRare", + "/Lotus/Objects/DomestikDrone/GrineerOceanDomestikDroneMover", + "/Lotus/Types/Gameplay/1999Wf/Extermination/SupplyCrate" ] From 5a56c2e9d32d2e37c2c4b90e43f39d4e55641621 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 17:35:52 -0700 Subject: [PATCH 230/776] feat: ascension ceremony inbox message (#1284) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1284 --- .../api/contributeGuildClassController.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index c5e878f7..629ec2ff 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -1,7 +1,8 @@ import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { Guild } from "@/src/models/guildModel"; +import { Guild, GuildMember } from "@/src/models/guildModel"; import { config } from "@/src/services/configService"; +import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; @@ -35,6 +36,37 @@ export const contributeGuildClassController: RequestHandler = async (req, res) = guild.Class = guild.CeremonyClass!; guild.CeremonyClass = undefined; guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); + if (!config.fastClanAscension) { + // Send message to all active guild members + const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId"); + for (const member of members) { + // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". + await createMessage(member.accountId.toString(), [ + { + sndr: guild.Name, + msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", + arg: [ + { + Key: "RESETDATE", + Tag: + guild.CeremonyResetDate.getUTCMonth() + + "/" + + guild.CeremonyResetDate.getUTCDate() + + "/" + + (guild.CeremonyResetDate.getUTCFullYear() % 100) + + " " + + guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") + + ":" + + guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0") + } + ], + sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", + icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", + highPriority: true + } + ]); + } + } } await guild.save(); From 5817b48db90814efbf8d78acb098fc64de7fe413 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 22 Mar 2025 18:12:59 -0700 Subject: [PATCH 231/776] fix: use deleteMany for models where accountId is not unique when deleting account (#1290) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1290 --- src/controllers/custom/deleteAccountController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index cb249d69..63ade312 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -14,12 +14,12 @@ export const deleteAccountController: RequestHandler = async (req, res) => { // TODO: Handle the account being the creator of a guild await Promise.all([ Account.deleteOne({ _id: accountId }), - GuildMember.deleteOne({ accountId: accountId }), + GuildMember.deleteMany({ accountId: accountId }), Inbox.deleteMany({ ownerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }), Loadout.deleteOne({ loadoutOwnerId: accountId }), PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }), - Ship.deleteOne({ ShipOwnerId: accountId }), + Ship.deleteMany({ ShipOwnerId: accountId }), Stats.deleteOne({ accountOwnerId: accountId }) ]); res.end(); From bc6f03b7c92eedcee99046c19dd839e03834c46f Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 05:06:06 -0700 Subject: [PATCH 232/776] feat: toggle wishlisted items (#1289) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1289 --- src/controllers/api/wishlistController.ts | 24 +++++++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 src/controllers/api/wishlistController.ts diff --git a/src/controllers/api/wishlistController.ts b/src/controllers/api/wishlistController.ts new file mode 100644 index 00000000..cfef2329 --- /dev/null +++ b/src/controllers/api/wishlistController.ts @@ -0,0 +1,24 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const wishlistController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Wishlist"); + const body = getJSONfromString(String(req.body)); + for (const item of body.WishlistItems) { + const i = inventory.Wishlist.findIndex(x => x == item); + if (i == -1) { + inventory.Wishlist.push(item); + } else { + inventory.Wishlist.splice(i, 1); + } + } + await inventory.save(); + res.end(); +}; + +interface IWishlistRequest { + WishlistItems: string[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 0a1c238b..aab62bda 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -115,6 +115,7 @@ import { updateSongChallengeController } from "@/src/controllers/api/updateSongC import { updateThemeController } from "@/src/controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; import { valenceSwapController } from "@/src/controllers/api/valenceSwapController"; +import { wishlistController } from "@/src/controllers/api/wishlistController"; const apiRouter = express.Router(); @@ -245,5 +246,6 @@ apiRouter.post("/updateSongChallenge.php", updateSongChallengeController); apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/upgrades.php", upgradesController); apiRouter.post("/valenceSwap.php", valenceSwapController); +apiRouter.post("/wishlist.php", wishlistController); export { apiRouter }; From e0d31b89880bed4f002c4c799290c27608a96065 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 05:06:31 -0700 Subject: [PATCH 233/776] feat: entratiLabConquestMode.php (#1291) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1291 --- .../api/entratiLabConquestModeController.ts | 69 +++++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 15 +++- src/routes/api.ts | 2 + src/types/inventoryTypes/inventoryTypes.ts | 11 +++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/entratiLabConquestModeController.ts diff --git a/src/controllers/api/entratiLabConquestModeController.ts b/src/controllers/api/entratiLabConquestModeController.ts new file mode 100644 index 00000000..ae7b5845 --- /dev/null +++ b/src/controllers/api/entratiLabConquestModeController.ts @@ -0,0 +1,69 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const entratiLabConquestModeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory( + accountId, + "EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission" + ); + const body = getJSONfromString(String(req.body)); + if (!inventory.EntratiVaultCountResetDate || Date.now() >= inventory.EntratiVaultCountResetDate.getTime()) { + const EPOCH = 1734307200 * 1000; // Mondays, amirite? + const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + inventory.EntratiVaultCountLastPeriod = 0; + inventory.EntratiVaultCountResetDate = new Date(weekEnd); + if (inventory.EntratiLabConquestUnlocked) { + inventory.EntratiLabConquestUnlocked = 0; + inventory.EntratiLabConquestActiveFrameVariants = []; + } + if (inventory.EchoesHexConquestUnlocked) { + inventory.EchoesHexConquestUnlocked = 0; + inventory.EchoesHexConquestActiveFrameVariants = []; + inventory.EchoesHexConquestActiveStickers = []; + } + } + if (body.BuyMode) { + inventory.EntratiVaultCountLastPeriod! += 2; + if (body.IsEchoesDeepArchemedea) { + inventory.EchoesHexConquestUnlocked = 1; + } else { + inventory.EntratiLabConquestUnlocked = 1; + } + } + if (body.IsEchoesDeepArchemedea) { + if (inventory.EchoesHexConquestUnlocked) { + inventory.EchoesHexConquestActiveFrameVariants = body.EchoesHexConquestActiveFrameVariants!; + inventory.EchoesHexConquestActiveStickers = body.EchoesHexConquestActiveStickers!; + } + } else { + if (inventory.EntratiLabConquestUnlocked) { + inventory.EntratiLabConquestActiveFrameVariants = body.EntratiLabConquestActiveFrameVariants!; + } + } + await inventory.save(); + res.json({ + EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate), + EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod, + EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked, + EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission, + EchoesHexConquestUnlocked: inventory.EchoesHexConquestUnlocked, + EchoesHexConquestCacheScoreMission: inventory.EchoesHexConquestCacheScoreMission + }); +}; + +interface IEntratiLabConquestModeRequest { + BuyMode?: number; + IsEchoesDeepArchemedea?: number; + EntratiLabConquestUnlocked?: number; + EntratiLabConquestActiveFrameVariants?: string[]; + EchoesHexConquestUnlocked?: number; + EchoesHexConquestActiveFrameVariants?: string[]; + EchoesHexConquestActiveStickers?: string[]; +} diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 8e24b4c6..ec0e1778 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1463,7 +1463,20 @@ const inventorySchema = new Schema( DialogueHistory: dialogueHistorySchema, CalendarProgress: calenderProgressSchema, - SongChallenges: { type: [songChallengeSchema], default: undefined } + SongChallenges: { type: [songChallengeSchema], default: undefined }, + + // Netracells + Deep Archimedea + EntratiVaultCountLastPeriod: { type: Number, default: undefined }, + EntratiVaultCountResetDate: { type: Date, default: undefined }, + EntratiLabConquestUnlocked: { type: Number, default: undefined }, + EntratiLabConquestHardModeStatus: { type: Number, default: undefined }, + EntratiLabConquestCacheScoreMission: { type: Number, default: undefined }, + EntratiLabConquestActiveFrameVariants: { type: [String], default: undefined }, + EchoesHexConquestUnlocked: { type: Number, default: undefined }, + EchoesHexConquestHardModeStatus: { type: Number, default: undefined }, + EchoesHexConquestCacheScoreMission: { type: Number, default: undefined }, + EchoesHexConquestActiveFrameVariants: { type: [String], default: undefined }, + EchoesHexConquestActiveStickers: { type: [String], default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); diff --git a/src/routes/api.ts b/src/routes/api.ts index aab62bda..9e573563 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -30,6 +30,7 @@ import { dojoComponentRushController } from "@/src/controllers/api/dojoComponent import { dojoController } from "@/src/controllers/api/dojoController"; import { dronesController } from "@/src/controllers/api/dronesController"; import { endlessXpController } from "@/src/controllers/api/endlessXpController"; +import { entratiLabConquestModeController } from "@/src/controllers/api/entratiLabConquestModeController"; import { evolveWeaponController } from "@/src/controllers/api/evolveWeaponController"; import { findSessionsController } from "@/src/controllers/api/findSessionsController"; import { fishmongerController } from "@/src/controllers/api/fishmongerController"; @@ -183,6 +184,7 @@ apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); +apiRouter.post("/entratiLabConquestMode.php", entratiLabConquestModeController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); apiRouter.post("/findSessions.php", findSessionsController); apiRouter.post("/fishmonger.php", fishmongerController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 59c1697a..27758acb 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -335,6 +335,17 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu DialogueHistory?: IDialogueHistoryClient; CalendarProgress: ICalendarProgress; SongChallenges?: ISongChallenge[]; + EntratiVaultCountLastPeriod?: number; + EntratiVaultCountResetDate?: Date; + EntratiLabConquestUnlocked?: number; + EntratiLabConquestHardModeStatus?: number; + EntratiLabConquestCacheScoreMission?: number; + EntratiLabConquestActiveFrameVariants?: string[]; + EchoesHexConquestUnlocked?: number; + EchoesHexConquestHardModeStatus?: number; + EchoesHexConquestCacheScoreMission?: number; + EchoesHexConquestActiveFrameVariants?: string[]; + EchoesHexConquestActiveStickers?: string[]; } export interface IAffiliation { From b5a0a2297e25498c6744c4fc36794e0bb5faada0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 05:07:15 -0700 Subject: [PATCH 234/776] feat: acquisition of peely pix + free pack for first visit (#1292) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1292 --- .../api/genericUpdateController.ts | 3 +- src/services/inventoryService.ts | 57 +++++- src/services/purchaseService.ts | 45 +++-- src/services/serversideVendorsService.ts | 2 + src/types/genericUpdate.ts | 7 + .../Nova1999ConquestShopManifest.json | 188 ++++++++++++++++++ 6 files changed, 282 insertions(+), 20 deletions(-) create mode 100644 static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json diff --git a/src/controllers/api/genericUpdateController.ts b/src/controllers/api/genericUpdateController.ts index 79b6ba44..e5f0b593 100644 --- a/src/controllers/api/genericUpdateController.ts +++ b/src/controllers/api/genericUpdateController.ts @@ -10,8 +10,7 @@ import { IGenericUpdate } from "@/src/types/genericUpdate"; const genericUpdateController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); const update = getJSONfromString(String(request.body)); - await updateGeneric(update, accountId); - response.json(update); + response.json(await updateGeneric(update, accountId)); }; export { genericUpdateController }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index e6bc21be..0f2b9ea5 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -28,7 +28,7 @@ import { IUpgradeClient, ICrewShipWeaponClient } from "@/src/types/inventoryTypes/inventoryTypes"; -import { IGenericUpdate } from "../types/genericUpdate"; +import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IMissionInventoryUpdateRequest, IThemeUpdateRequest, @@ -574,6 +574,39 @@ export const addItem = async ( }; } break; + + case "Stickers": + { + const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName); + if (entry && entry.ItemCount >= 10) { + const miscItemChanges = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + ItemCount: 1 + } + ]; + addMiscItems(inventory, miscItemChanges); + return { + InventoryChanges: { + MiscItems: miscItemChanges + } + }; + } else { + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addMods(inventory, changes); + return { + InventoryChanges: { + RawUpgrades: changes + } + }; + } + } + break; } break; } @@ -876,14 +909,27 @@ export const updateStandingLimit = ( }; // TODO: AffiliationMods support (Nightwave). -export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { - const inventory = await getInventory(accountId); +export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { + const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems"); // Make it an array for easier parsing. if (typeof data.NodeIntrosCompleted === "string") { data.NodeIntrosCompleted = [data.NodeIntrosCompleted]; } + const inventoryChanges: IInventoryChanges = {}; + for (const node of data.NodeIntrosCompleted) { + if (node == "KayaFirstVisitPack") { + inventoryChanges.MiscItems = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", + ItemCount: 1 + } + ]; + addMiscItems(inventory, inventoryChanges.MiscItems); + } + } + // Combine the two arrays into one. data.NodeIntrosCompleted = inventory.NodeIntrosCompleted.concat(data.NodeIntrosCompleted); @@ -892,6 +938,11 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr inventory.NodeIntrosCompleted = nodes; await inventory.save(); + + return { + MissionRewards: [], + InventoryChanges: inventoryChanges + }; }; export const updateTheme = async (data: IThemeUpdateRequest, accountId: string): Promise => { diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index ef7a97b7..72e1701c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -54,11 +54,16 @@ export const handlePurchase = async ( if (purchaseRequest.PurchaseParams.Source == 7) { const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (manifest) { - const ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson!) as { ItemId: string }) - .ItemId; - const offer = manifest.VendorInfo.ItemManifest.find(x => x.Id.$oid == ItemId); + let ItemId: string | undefined; + if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { + ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) + .ItemId; + } + const offer = ItemId + ? manifest.VendorInfo.ItemManifest.find(x => x.Id.$oid == ItemId) + : manifest.VendorInfo.ItemManifest.find(x => x.StoreItem == purchaseRequest.PurchaseParams.StoreItem); if (!offer) { - throw new Error(`unknown vendor offer: ${ItemId}`); + throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`); } if (offer.ItemPrices) { handleItemPrices( @@ -68,7 +73,7 @@ export const handlePurchase = async ( prePurchaseInventoryChanges ); } - if (!config.noVendorPurchaseLimits) { + if (!config.noVendorPurchaseLimits && ItemId) { inventory.RecentVendorPurchases ??= []; let vendorPurchases = inventory.RecentVendorPurchases.find( x => x.VendorType == manifest.VendorInfo.TypeName @@ -410,16 +415,26 @@ const handleBoosterPackPurchase = async ( "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server." ); } - for (let i = 0; i != quantity; ++i) { - for (const weights of pack.rarityWeightsPerRoll) { - const result = getRandomWeightedRewardUc(pack.components, weights); - if (result) { - logger.debug(`booster pack rolled`, result); - purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; - combineInventoryChanges( - purchaseResponse.InventoryChanges, - (await addItem(inventory, result.Item, 1)).InventoryChanges - ); + if (typeName == "/Lotus/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed") { + for (const result of pack.components) { + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; + combineInventoryChanges( + purchaseResponse.InventoryChanges, + (await addItem(inventory, result.Item, 1)).InventoryChanges + ); + } + } else { + for (let i = 0; i != quantity; ++i) { + for (const weights of pack.rarityWeightsPerRoll) { + const result = getRandomWeightedRewardUc(pack.components, weights); + if (result) { + logger.debug(`booster pack rolled`, result); + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; + combineInventoryChanges( + purchaseResponse.InventoryChanges, + (await addItem(inventory, result.Item, 1)).InventoryChanges + ); + } } } } diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index cae48e6e..79e22ef3 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -19,6 +19,7 @@ import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorI import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json"; import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; +import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; @@ -69,6 +70,7 @@ const vendorManifests: IVendorManifest[] = [ HubsPerrinSequenceWeaponVendorManifest, HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, + Nova1999ConquestShopManifest, OstronFishmongerVendorManifest, OstronPetVendorManifest, OstronProspectorVendorManifest, diff --git a/src/types/genericUpdate.ts b/src/types/genericUpdate.ts index fa231be9..93551b05 100644 --- a/src/types/genericUpdate.ts +++ b/src/types/genericUpdate.ts @@ -1,4 +1,11 @@ +import { IInventoryChanges } from "./purchaseTypes"; + export interface IGenericUpdate { NodeIntrosCompleted: string | string[]; // AffiliationMods: any[]; } + +export interface IUpdateNodeIntrosResponse { + MissionRewards: []; + InventoryChanges: IInventoryChanges; +} diff --git a/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json b/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json new file mode 100644 index 00000000..b2971efe --- /dev/null +++ b/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json @@ -0,0 +1,188 @@ +{ + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d6a" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Nova1999ConquestShopManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18c" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFree", + "ItemPrices": [ + { + "ItemCount": 1, + "ItemType": "/Lotus/Types/Items/MiscItems/1999FreeStickersPack", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed", + "ItemPrices": [ + { + "ItemCount": 1, + "ItemType": "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18e" + } + }, + { + "StoreItem": "/Lotus/Types/StoreItems/Packages/SyndicateVosforPack", + "ItemPrices": [ + { + "ItemCount": 6, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/StickerPictureFrame", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c190" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Utility/AbilityRadiationProcsCreateUniversalOrbsOnKill", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c191" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Offensive/AbilityHeatProcsGiveCritChance", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c192" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Defensive/InvulnerabilityOnDeathOnMercyKill", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c193" + } + } + ], + "PropertyTextHash": "CB7D0E807FD5E2BCD059195201D963B9", + "RequiredGoalTag": "", + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + } + } +} \ No newline at end of file From 5277f7cc3781d45bae899b4c45b4927ea66c3f69 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 05:20:48 -0700 Subject: [PATCH 235/776] feat(import): loc pins (#1297) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1297 --- src/services/importService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/importService.ts b/src/services/importService.ts index 979221c8..535b67b7 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -239,6 +239,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< if (client.DialogueHistory !== undefined) { db.DialogueHistory = convertDialogueHistory(client.DialogueHistory); } + if (client.CustomMarkers !== undefined) { + db.CustomMarkers = client.CustomMarkers; + } }; const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { From cf125b53557de9f02cd5391ab848c8e87bc07bc1 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 13:59:22 +0100 Subject: [PATCH 236/776] chore: prettier --- .../Nova1999ConquestShopManifest.json | 366 +++++++++--------- 1 file changed, 183 insertions(+), 183 deletions(-) diff --git a/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json b/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json index b2971efe..59afcd65 100644 --- a/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json +++ b/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json @@ -1,188 +1,188 @@ { - "VendorInfo": { - "_id": { - "$oid": "67dadc30e4b6e0e5979c8d6a" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Nova1999ConquestShopManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "67db32b983b2ad79a9c1c18c" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFree", - "ItemPrices": [ - { - "ItemCount": 1, - "ItemType": "/Lotus/Types/Items/MiscItems/1999FreeStickersPack", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "67db32b983b2ad79a9c1c18d" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed", - "ItemPrices": [ - { - "ItemCount": 1, - "ItemType": "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "67db32b983b2ad79a9c1c18e" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/SyndicateVosforPack", - "ItemPrices": [ - { - "ItemCount": 6, - "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "67db32b983b2ad79a9c1c18f" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/StickerPictureFrame", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "67db32b983b2ad79a9c1c190" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Utility/AbilityRadiationProcsCreateUniversalOrbsOnKill", - "ItemPrices": [ - { - "ItemCount": 5, - "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "Id": { - "$oid": "67db32b983b2ad79a9c1c191" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Offensive/AbilityHeatProcsGiveCritChance", - "ItemPrices": [ - { - "ItemCount": 5, - "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "Id": { - "$oid": "67db32b983b2ad79a9c1c192" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Defensive/InvulnerabilityOnDeathOnMercyKill", - "ItemPrices": [ - { - "ItemCount": 5, - "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "Id": { - "$oid": "67db32b983b2ad79a9c1c193" - } - } + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d6a" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Nova1999ConquestShopManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } ], - "PropertyTextHash": "CB7D0E807FD5E2BCD059195201D963B9", - "RequiredGoalTag": "", + "Bin": "BIN_0", + "QuantityMultiplier": 1, "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18c" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFree", + "ItemPrices": [ + { + "ItemCount": 1, + "ItemType": "/Lotus/Types/Items/MiscItems/1999FreeStickersPack", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed", + "ItemPrices": [ + { + "ItemCount": 1, + "ItemType": "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18e" + } + }, + { + "StoreItem": "/Lotus/Types/StoreItems/Packages/SyndicateVosforPack", + "ItemPrices": [ + { + "ItemCount": 6, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/StickerPictureFrame", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c190" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Utility/AbilityRadiationProcsCreateUniversalOrbsOnKill", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c191" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Offensive/AbilityHeatProcsGiveCritChance", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c192" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Defensive/InvulnerabilityOnDeathOnMercyKill", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c193" + } + } + ], + "PropertyTextHash": "CB7D0E807FD5E2BCD059195201D963B9", + "RequiredGoalTag": "", + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } } -} \ No newline at end of file + } +} From aa127087383cc24311c24337fe9c1d6261d39177 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 08:26:46 -0700 Subject: [PATCH 237/776] chore: make addItem return InventoryChanges directly (#1299) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1299 --- .../api/claimCompletedRecipeController.ts | 2 +- .../api/giveStartingGearController.ts | 2 +- src/controllers/api/guildTechController.ts | 2 +- src/services/inventoryService.ts | 193 +++++++----------- src/services/purchaseService.ts | 11 +- 5 files changed, 76 insertions(+), 134 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 5b4f1acb..b796b53b 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -118,7 +118,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } InventoryChanges = { ...InventoryChanges, - ...(await addItem(inventory, recipe.resultType, recipe.num, false)).InventoryChanges + ...(await addItem(inventory, recipe.resultType, recipe.num, false)) }; await inventory.save(); res.json({ InventoryChanges }); diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index b8b09b2e..93fa1452 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -87,7 +87,7 @@ export const addStartingGear = async ( for (const item of awakeningRewards) { const inventoryDelta = await addItem(inventory, item); - combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); + combineInventoryChanges(inventoryChanges, inventoryDelta); } inventory.PlayedParkourTutorial = true; diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index e1e79e8f..2faf34c7 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -194,7 +194,7 @@ export const guildTechController: RequestHandler = async (req, res) => { ItemCount: x.ItemCount * -1 })); addMiscItems(inventory, inventoryChanges.MiscItems); - combineInventoryChanges(inventoryChanges, (await addItem(inventory, recipe.resultType)).InventoryChanges); + combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType)); await inventory.save(); // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. res.json({ inventoryChanges: inventoryChanges }); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 0f2b9ea5..ef99f53a 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -234,10 +234,10 @@ export const addItem = async ( typeName: string, quantity: number = 1, premiumPurchase: boolean = false -): Promise<{ InventoryChanges: IInventoryChanges }> => { +): Promise => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { - return { InventoryChanges: await handleBundleAcqusition(typeName, inventory, quantity) }; + return await handleBundleAcqusition(typeName, inventory, quantity); } // Strict typing @@ -250,9 +250,7 @@ export const addItem = async ( ]; addRecipes(inventory, recipeChanges); return { - InventoryChanges: { - Recipes: recipeChanges - } + Recipes: recipeChanges }; } if (typeName in ExportResources) { @@ -265,9 +263,7 @@ export const addItem = async ( ]; addMiscItems(inventory, miscItemChanges); return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } else if (ExportResources[typeName].productCategory == "FusionTreasures") { const fusionTreasureChanges = [ @@ -279,25 +275,21 @@ export const addItem = async ( ]; addFusionTreasures(inventory, fusionTreasureChanges); return { - InventoryChanges: { - FusionTreasures: fusionTreasureChanges - } + FusionTreasures: fusionTreasureChanges }; } else if (ExportResources[typeName].productCategory == "Ships") { const oid = await createShip(inventory.accountOwnerId, typeName); inventory.Ships.push(oid); return { - InventoryChanges: { - Ships: [ - { - ItemId: { $oid: oid.toString() }, - ItemType: typeName - } - ] - } + Ships: [ + { + ItemId: { $oid: oid.toString() }, + ItemType: typeName + } + ] }; } else if (ExportResources[typeName].productCategory == "CrewShips") { - const inventoryChanges = { + return { ...addCrewShip(inventory, typeName), // fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -305,8 +297,6 @@ export const addItem = async ( ? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") : {}) }; - - return { InventoryChanges: inventoryChanges }; } else if (ExportResources[typeName].productCategory == "ShipDecorations") { const changes = [ { @@ -316,9 +306,7 @@ export const addItem = async ( ]; addShipDecorations(inventory, changes); return { - InventoryChanges: { - ShipDecorations: changes - } + ShipDecorations: changes }; } else if (ExportResources[typeName].productCategory == "KubrowPetEggs") { const changes: IKubrowPetEggClient[] = []; @@ -339,9 +327,7 @@ export const addItem = async ( }); } return { - InventoryChanges: { - KubrowPetEggs: changes - } + KubrowPetEggs: changes }; } else { throw new Error(`unknown product category: ${ExportResources[typeName].productCategory}`); @@ -349,14 +335,13 @@ export const addItem = async ( } if (typeName in ExportCustoms) { if (ExportCustoms[typeName].productCategory == "CrewShipWeaponSkins") { - return { InventoryChanges: addCrewShipWeaponSkin(inventory, typeName) }; + return addCrewShipWeaponSkin(inventory, typeName); } else { - return { InventoryChanges: addSkin(inventory, typeName) }; + return addSkin(inventory, typeName); } } if (typeName in ExportFlavour) { - const inventoryChanges = addCustomization(inventory, typeName); - return { InventoryChanges: inventoryChanges }; + return addCustomization(inventory, typeName); } if (typeName in ExportUpgrades || typeName in ExportArcanes) { const changes = [ @@ -367,9 +352,7 @@ export const addItem = async ( ]; addMods(inventory, changes); return { - InventoryChanges: { - RawUpgrades: changes - } + RawUpgrades: changes }; } if (typeName in ExportGear) { @@ -381,9 +364,7 @@ export const addItem = async ( ]; addConsumables(inventory, consumablesChanges); return { - InventoryChanges: { - Consumables: consumablesChanges - } + Consumables: consumablesChanges }; } if (typeName in ExportWeapons) { @@ -426,14 +407,12 @@ export const addItem = async ( ); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { - combineInventoryChanges(inventoryChanges, (await addItem(inventory, item, 1)).InventoryChanges); + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); } } return { - InventoryChanges: { - ...inventoryChanges, - ...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase) - } + ...inventoryChanges, + ...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase) }; } else { // Modular weapon parts @@ -445,36 +424,28 @@ export const addItem = async ( ]; addMiscItems(inventory, miscItemChanges); return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } } if (typeName in ExportRailjackWeapons) { return { - InventoryChanges: { - ...addCrewShipWeapon(inventory, typeName), - ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) - } + ...addCrewShipWeapon(inventory, typeName), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) }; } if (typeName in ExportMisc.creditBundles) { const creditsTotal = ExportMisc.creditBundles[typeName] * quantity; inventory.RegularCredits += creditsTotal; return { - InventoryChanges: { - RegularCredits: creditsTotal - } + RegularCredits: creditsTotal }; } if (typeName in ExportFusionBundles) { const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity; inventory.FusionPoints += fusionPointsTotal; return { - InventoryChanges: { - FusionPoints: fusionPointsTotal - } + FusionPoints: fusionPointsTotal }; } if (typeName in ExportKeys) { @@ -483,8 +454,8 @@ export const addItem = async ( if (key.chainStages) { const key = addQuestKey(inventory, { ItemType: typeName }); - if (!key) return { InventoryChanges: {} }; - return { InventoryChanges: { QuestKeys: [key] } }; + if (!key) return {}; + return { QuestKeys: [key] }; } else { const key = { ItemType: typeName, ItemCount: quantity }; @@ -494,19 +465,14 @@ export const addItem = async ( } else { inventory.LevelKeys.push(key); } - return { InventoryChanges: { LevelKeys: [key] } }; + return { LevelKeys: [key] }; } } if (typeName in ExportDrones) { - const inventoryChanges = addDrone(inventory, typeName); - return { - InventoryChanges: inventoryChanges - }; + return addDrone(inventory, typeName); } if (typeName in ExportEmailItems) { - return { - InventoryChanges: await addEmailItem(inventory, typeName) - }; + return await addEmailItem(inventory, typeName); } // Path-based duck typing @@ -515,42 +481,36 @@ export const addItem = async ( switch (typeName.substr(1).split("/")[2]) { default: { return { - InventoryChanges: { - ...addPowerSuit( - inventory, - typeName, - {}, - premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - ), - ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) - } + ...addPowerSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), + ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) }; } case "Archwing": { inventory.ArchwingEnabled = true; return { - InventoryChanges: { - ...addSpaceSuit( - inventory, - typeName, - {}, - premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - ), - ...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase) - } + ...addSpaceSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), + ...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase) }; } case "EntratiMech": { return { - InventoryChanges: { - ...addMechSuit( - inventory, - typeName, - {}, - premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - ), - ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) - } + ...addMechSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), + ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) }; } } @@ -568,9 +528,7 @@ export const addItem = async ( ]; addMods(inventory, changes); return { - InventoryChanges: { - RawUpgrades: changes - } + RawUpgrades: changes }; } break; @@ -587,9 +545,7 @@ export const addItem = async ( ]; addMiscItems(inventory, miscItemChanges); return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } else { const changes = [ @@ -600,9 +556,7 @@ export const addItem = async ( ]; addMods(inventory, changes); return { - InventoryChanges: { - RawUpgrades: changes - } + RawUpgrades: changes }; } } @@ -613,9 +567,7 @@ export const addItem = async ( case "Types": switch (typeName.substr(1).split("/")[2]) { case "Sentinels": { - return { - InventoryChanges: addSentinel(inventory, typeName, premiumPurchase) - }; + return addSentinel(inventory, typeName, premiumPurchase); } case "Game": { if (typeName.substr(1).split("/")[3] == "Projections") { @@ -629,9 +581,7 @@ export const addItem = async ( addMiscItems(inventory, miscItemChanges); inventory.HasOwnedVoidProjectionsPreviously = true; return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } break; @@ -639,27 +589,23 @@ export const addItem = async ( case "NeutralCreatures": { const horseIndex = inventory.Horses.push({ ItemType: typeName }); return { - InventoryChanges: { - Horses: [inventory.Horses[horseIndex - 1].toJSON()] - } + Horses: [inventory.Horses[horseIndex - 1].toJSON()] }; } case "Recipes": { inventory.MiscItems.push({ ItemType: typeName, ItemCount: quantity }); return { - InventoryChanges: { - MiscItems: [ - { - ItemType: typeName, - ItemCount: quantity - } - ] - } + MiscItems: [ + { + ItemType: typeName, + ItemCount: quantity + } + ] }; } case "Vehicles": if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") { - return { InventoryChanges: addMotorcycle(inventory, typeName) }; + return addMotorcycle(inventory, typeName); } break; } @@ -680,7 +626,7 @@ export const addItems = async ( } else { inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount, true); } - combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); + combineInventoryChanges(inventoryChanges, inventoryDelta); } return inventoryChanges; }; @@ -1388,12 +1334,11 @@ export const addKeyChainItems = async ( const nonStoreItems = keyChainItems.map(item => fromStoreItem(item)); - //TODO: inventoryChanges is not typed correctly - const inventoryChanges = {}; + const inventoryChanges: IInventoryChanges = {}; for (const item of nonStoreItems) { const inventoryChangesDelta = await addItem(inventory, item); - combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges); + combineInventoryChanges(inventoryChanges, inventoryChangesDelta); } return inventoryChanges; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 72e1701c..50720d6c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -333,7 +333,7 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = await addItem(inventory, internalName, quantity, true); + purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true) }; break; } case "Types": @@ -418,10 +418,7 @@ const handleBoosterPackPurchase = async ( if (typeName == "/Lotus/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed") { for (const result of pack.components) { purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; - combineInventoryChanges( - purchaseResponse.InventoryChanges, - (await addItem(inventory, result.Item, 1)).InventoryChanges - ); + combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1)); } } else { for (let i = 0; i != quantity; ++i) { @@ -432,7 +429,7 @@ const handleBoosterPackPurchase = async ( purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; combineInventoryChanges( purchaseResponse.InventoryChanges, - (await addItem(inventory, result.Item, 1)).InventoryChanges + await addItem(inventory, result.Item, 1) ); } } @@ -468,7 +465,7 @@ const handleTypesPurchase = async ( logger.debug(`type category ${typeCategory}`); switch (typeCategory) { default: - return await addItem(inventory, typesName, quantity); + return { InventoryChanges: await addItem(inventory, typesName, quantity) }; case "BoosterPacks": return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": From c3d7ae33c2d44db0ca49e2193cae3c2f33027b6e Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 16:40:48 +0100 Subject: [PATCH 238/776] chore: do 'git stash' before hard reset Just in case the user made local changes and then runs the bat we don't wanna have it be irrecoverably lost. --- UPDATE AND START SERVER.bat | 1 + 1 file changed, 1 insertion(+) diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index 8fe5b00c..d0937399 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -3,6 +3,7 @@ echo Updating SpaceNinjaServer... git config remote.origin.url https://openwf.io/SpaceNinjaServer.git git fetch --prune +git stash git reset --hard origin/main if exist static\data\0\ ( From 7f5592e00ca99861cd2fd61a65f0c8765131ff6f Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 09:05:47 -0700 Subject: [PATCH 239/776] chore: improve authentication and Dropped logic (#1296) - Dropped is now also unset by getAccountForRequest - Improved how nonce is validated to avoid possible parser mismatch issues to smuggle a 0 - Updated ircDroppedController to perform only a single MongoDB operation Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1296 --- .../custom/ircDroppedController.ts | 23 +++++++++++++++---- src/services/loginService.ts | 17 +++++++------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/controllers/custom/ircDroppedController.ts b/src/controllers/custom/ircDroppedController.ts index 8927c5bb..1621defc 100644 --- a/src/controllers/custom/ircDroppedController.ts +++ b/src/controllers/custom/ircDroppedController.ts @@ -1,9 +1,24 @@ -import { getAccountForRequest } from "@/src/services/loginService"; +import { Account } from "@/src/models/loginModel"; import { RequestHandler } from "express"; export const ircDroppedController: RequestHandler = async (req, res) => { - const account = await getAccountForRequest(req); - account.Dropped = true; - await account.save(); + if (!req.query.accountId) { + throw new Error("Request is missing accountId parameter"); + } + const nonce: number = parseInt(req.query.nonce as string); + if (!nonce) { + throw new Error("Request is missing nonce parameter"); + } + + await Account.updateOne( + { + _id: req.query.accountId, + Nonce: nonce + }, + { + Dropped: true + } + ); + res.end(); }; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 099103be..6509d8be 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -69,26 +69,27 @@ export const getAccountForRequest = async (req: Request): Promise => { - const account = await getAccountForRequest(req); - if (account.Dropped && req.query.ct) { - account.Dropped = undefined; - await account.save(); - } - return account._id.toString(); + return (await getAccountForRequest(req))._id.toString(); }; export const isAdministrator = (account: TAccountDocument): boolean => { From cf3007b744187a8338d8bb395652cfe7bf8b8808 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 09:06:08 -0700 Subject: [PATCH 240/776] chore: update config when admin changes their name (#1298) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1298 --- src/controllers/custom/renameAccountController.ts | 13 ++++++++++++- src/index.ts | 10 +++++++--- src/services/configService.ts | 13 +++++++++---- src/services/loginService.ts | 8 +------- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/controllers/custom/renameAccountController.ts b/src/controllers/custom/renameAccountController.ts index c5b733e8..d30b44ae 100644 --- a/src/controllers/custom/renameAccountController.ts +++ b/src/controllers/custom/renameAccountController.ts @@ -1,5 +1,6 @@ import { RequestHandler } from "express"; -import { getAccountForRequest, isNameTaken } from "@/src/services/loginService"; +import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; +import { config, saveConfig } from "@/src/services/configService"; export const renameAccountController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); @@ -7,8 +8,18 @@ export const renameAccountController: RequestHandler = async (req, res) => { if (await isNameTaken(req.query.newname)) { res.status(409).json("Name already in use"); } else { + if (isAdministrator(account)) { + for (let i = 0; i != config.administratorNames!.length; ++i) { + if (config.administratorNames![i] == account.DisplayName) { + config.administratorNames![i] = req.query.newname; + } + } + await saveConfig(); + } + account.DisplayName = req.query.newname; await account.save(); + res.end(); } } else { diff --git a/src/index.ts b/src/index.ts index 4f7fc939..f540f96e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,9 +19,13 @@ import mongoose from "mongoose"; return "" + this.toString() + ""; }; const og_stringify = JSON.stringify; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (JSON as any).stringify = (obj: any): string => { - return og_stringify(obj).split(`"`).join(``).split(`"`).join(``); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + JSON.stringify = (obj: any, replacer?: any, space?: string | number): string => { + return og_stringify(obj, replacer as string[], space) + .split(`"`) + .join(``) + .split(`"`) + .join(``); }; } diff --git a/src/services/configService.ts b/src/services/configService.ts index 66c50dda..114eccc9 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -34,7 +34,7 @@ interface IConfig { httpsPort?: number; myIrcAddresses?: string[]; NRS?: string[]; - administratorNames?: string[] | string; + administratorNames?: string[]; autoCreateAccount?: boolean; skipTutorial?: boolean; skipAllDialogue?: boolean; @@ -83,10 +83,15 @@ export const updateConfig = async (data: string): Promise => { Object.assign(config, JSON.parse(data)); }; +export const saveConfig = async (): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); +}; + export const validateConfig = (): void => { if (typeof config.administratorNames == "string") { - logger.warn( - `"administratorNames" should be an array; please add square brackets: ["${config.administratorNames}"]` - ); + logger.info(`Updating config.json to make administratorNames an array.`); + config.administratorNames = [config.administratorNames]; + void saveConfig(); } }; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 6509d8be..c69e891d 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -93,13 +93,7 @@ export const getAccountIdForRequest = async (req: Request): Promise => { }; export const isAdministrator = (account: TAccountDocument): boolean => { - if (!config.administratorNames) { - return false; - } - if (typeof config.administratorNames == "string") { - return config.administratorNames == account.DisplayName; - } - return !!config.administratorNames.find(x => x == account.DisplayName); + return !!config.administratorNames?.find(x => x == account.DisplayName); }; const platform_magics = [753, 639, 247, 37, 60]; From 8a29f06207243f8557fc10ebea7e87b544a19aab Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 09:06:28 -0700 Subject: [PATCH 241/776] chore: use inventory projection for updateTheme (#1302) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1302 --- src/controllers/api/updateThemeController.ts | 28 +++++++++----------- src/services/inventoryService.ts | 15 +---------- src/types/requestTypes.ts | 6 ----- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/src/controllers/api/updateThemeController.ts b/src/controllers/api/updateThemeController.ts index ccb4ab57..ce31d27d 100644 --- a/src/controllers/api/updateThemeController.ts +++ b/src/controllers/api/updateThemeController.ts @@ -1,25 +1,23 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { updateTheme } from "@/src/services/inventoryService"; -import { IThemeUpdateRequest } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; +import { getInventory } from "@/src/services/inventoryService"; -const updateThemeController: RequestHandler = async (request, response) => { +export const updateThemeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); - const body = String(request.body); + const data = getJSONfromString(String(request.body)); - try { - const json = getJSONfromString(body); - if (typeof json !== "object") { - throw new Error("Invalid data format"); - } - - await updateTheme(json, accountId); - } catch (err) { - console.error("Error parsing JSON data:", err); - } + const inventory = await getInventory(accountId, "ThemeStyle ThemeBackground ThemeSounds"); + if (data.Style) inventory.ThemeStyle = data.Style; + if (data.Background) inventory.ThemeBackground = data.Background; + if (data.Sounds) inventory.ThemeSounds = data.Sounds; + await inventory.save(); response.json({}); }; -export { updateThemeController }; +interface IThemeUpdateRequest { + Style?: string; + Background?: string; + Sounds?: string; +} diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ef99f53a..ffa18484 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -29,11 +29,7 @@ import { ICrewShipWeaponClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; -import { - IMissionInventoryUpdateRequest, - IThemeUpdateRequest, - IUpdateChallengeProgressRequest -} from "../types/requestTypes"; +import { IMissionInventoryUpdateRequest, IUpdateChallengeProgressRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { convertInboxMessage, fromStoreItem, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; import { @@ -891,15 +887,6 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr }; }; -export const updateTheme = async (data: IThemeUpdateRequest, accountId: string): Promise => { - const inventory = await getInventory(accountId); - if (data.Style) inventory.ThemeStyle = data.Style; - if (data.Background) inventory.ThemeBackground = data.Background; - if (data.Sounds) inventory.ThemeSounds = data.Sounds; - - await inventory.save(); -}; - export const addEquipment = ( inventory: TInventoryDatabaseDocument, category: TEquipmentKey, diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index f9df3ff8..78e43c5c 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -19,12 +19,6 @@ import { ICollectibleEntry } from "./inventoryTypes/inventoryTypes"; -export interface IThemeUpdateRequest { - Style?: string; - Background?: string; - Sounds?: string; -} - export interface IAffiliationChange { Tag: string; Standing: number; From d0df9e3731de6d0343abbf61d30c83e980cc9a68 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 21:05:31 +0100 Subject: [PATCH 242/776] chore: remove unused junctionRewards.json --- static/fixed_responses/junctionRewards.json | 120 -------------------- 1 file changed, 120 deletions(-) delete mode 100644 static/fixed_responses/junctionRewards.json diff --git a/static/fixed_responses/junctionRewards.json b/static/fixed_responses/junctionRewards.json deleted file mode 100644 index e09c56a6..00000000 --- a/static/fixed_responses/junctionRewards.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "VenusToMercuryJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedIntroQuest/InfestedIntroQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/KubrowQuest/KubrowQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Rifle/BoltoRifle", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarShieldRechargeRateMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarAbilityEfficiencyMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/EggHatcher", "ItemCount": 1 } - ], - "credits": 10000 - }, - "EarthToVenusJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/FurisBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponFreezeDamageMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponElectricityDamageMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/SentinelRecipes/TnSentinelCrossBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/StaffCmbOneMeleeTree", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerEnergyHealthRegenAuraMod", "ItemCount": 1 } - ], - "credits": 5000 - }, - "EarthToMarsJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/EggHatcher", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/VoidProjectionFeatureItem", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionRevenantPrimeABronze", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/HammerWeapon", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/IronPhoenixMeleeTree", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain", "ItemCount": 1 } - ], - "credits": 15000 - }, - "MarsToCeresJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnSniperRifleBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponToxinDamageMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/DualSwordCmbOneMeleeTree", "ItemCount": 1 } - ], - "credits": 20000 - }, - "MarsToPhobosJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/SpyQuestKeyChain/SpyQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnHeavyPistolBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/StoreItems/Consumables/CipherBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponReloadSpeedMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarLootRadarMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst", "ItemCount": 1 } - ], - "credits": 20000 - }, - "JupiterToEuropaJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/LimboQuest/LimboQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/DragonQuest/DragonQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/CorpusMinigunBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", "ItemCount": 1 } - ], - "credits": 40000 - }, - "JupiterToSaturnJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrenadeLauncherBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/ProteaQuest/ProteaQuestKeyChain", "ItemCount": 1 } - ], - "credits": 40000 - }, - "SaturnToUranusJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/CorpusWhipBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/DuviriQuest/DuviriQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", "ItemCount": 1 } - ], - "credits": 60000 - }, - "UranusToNeptuneJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/ReconnasorBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint", "ItemCount": 1 } - ], - "credits": 80000 - }, - "NeptuneToPlutoJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrineerFlakCannonBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint", "ItemCount": 1 } - ], - "credits": 80000 - }, - "PlutoToSednaJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/WarWithinQuest/WarWithinQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/MirageQuest/MirageQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/DualDaggerBlueprint", "ItemCount": 1 } - ], - "credits": 100000 - }, - "PlutoToErisJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedAladVQuest/InfestedAladVQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/MireSwordBlueprint", "ItemCount": 1 } - ], - "credits": 100000 - }, - "CeresToJupiterJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnStaffBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Suit/ArchwingSuitHealthMaxMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/ArchwingRifleDamageAmountMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Melee/ArchwingMeleeDamageMod", "ItemCount": 1 } - ], - "credits": 30000 - } -} From 19bfffaa7c851f3002669237279400d111d505ab Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 13:09:02 -0700 Subject: [PATCH 243/776] fix: give helmet when acquiring a skin (#1304) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1304 --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/inventoryService.ts | 14 +++++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fdbe6b6..7fbf5002 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.47", + "warframe-public-export-plus": "^0.5.48", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -4013,9 +4013,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.47", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.47.tgz", - "integrity": "sha512-ZJK3VT1PdSPwZlhIzUVBlydwK4DM0sOmeCiixVMgOM8XuOPJ8OHfQUoLKydtw5rxCsowzFPbx5b3KBke5C4akQ==" + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.48.tgz", + "integrity": "sha512-vJitVYnaViQo43xAkL/h3MJ/6wS7YknKEYhYs+N/GrsspYLMPGf9KSuR19tprB2g9KVGS5o67t0v5K8p0RTQCQ==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 3d0ade47..0cd17cb3 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.47", + "warframe-public-export-plus": "^0.5.48", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ffa18484..74b64a44 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -330,11 +330,19 @@ export const addItem = async ( } } if (typeName in ExportCustoms) { - if (ExportCustoms[typeName].productCategory == "CrewShipWeaponSkins") { - return addCrewShipWeaponSkin(inventory, typeName); + const meta = ExportCustoms[typeName]; + let inventoryChanges: IInventoryChanges; + if (meta.productCategory == "CrewShipWeaponSkins") { + inventoryChanges = addCrewShipWeaponSkin(inventory, typeName); } else { - return addSkin(inventory, typeName); + inventoryChanges = addSkin(inventory, typeName); } + if (meta.additionalItems) { + for (const item of meta.additionalItems) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item)); + } + } + return inventoryChanges; } if (typeName in ExportFlavour) { return addCustomization(inventory, typeName); From db8bff20fef5f439470373d377b894d66b031e66 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 13:09:13 -0700 Subject: [PATCH 244/776] fix: only roll unique rewards for peely pix booster packs (#1306) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1306 --- src/services/purchaseService.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 50720d6c..c17b562f 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -415,24 +415,24 @@ const handleBoosterPackPurchase = async ( "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server." ); } - if (typeName == "/Lotus/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed") { - for (const result of pack.components) { - purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; - combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1)); - } - } else { - for (let i = 0; i != quantity; ++i) { - for (const weights of pack.rarityWeightsPerRoll) { - const result = getRandomWeightedRewardUc(pack.components, weights); - if (result) { - logger.debug(`booster pack rolled`, result); - purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; - combineInventoryChanges( - purchaseResponse.InventoryChanges, - await addItem(inventory, result.Item, 1) - ); + for (let i = 0; i != quantity; ++i) { + const disallowedItems = new Set(); + for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) { + const weights = pack.rarityWeightsPerRoll[roll]; + const result = getRandomWeightedRewardUc(pack.components, weights); + if (result) { + logger.debug(`booster pack rolled`, result); + if (disallowedItems.has(result.Item)) { + logger.debug(`oops, can't use that one; trying again`); + continue; } + if (!pack.canGiveDuplicates) { + disallowedItems.add(result.Item); + } + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; + combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1)); } + ++roll; } } return purchaseResponse; From e7605a2e17c823098ffa1b97daee00606a33e1d0 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 13:09:38 -0700 Subject: [PATCH 245/776] fix: use IMongoDate for EntratiVaultCountResetDate in inventory response (#1308) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1308 --- src/models/inventoryModels/inventoryModel.ts | 3 +++ src/types/inventoryTypes/inventoryTypes.ts | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index ec0e1778..198c68f7 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1505,6 +1505,9 @@ inventorySchema.set("toJSON", { if (inventoryDatabase.NextRefill) { inventoryResponse.NextRefill = toMongoDate(inventoryDatabase.NextRefill); } + if (inventoryDatabase.EntratiVaultCountResetDate) { + inventoryResponse.EntratiVaultCountResetDate = toMongoDate(inventoryDatabase.EntratiVaultCountResetDate); + } } }); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 27758acb..54d50cc1 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -44,6 +44,7 @@ export interface IInventoryDatabase | "RecentVendorPurchases" | "NextRefill" | "Nemesis" + | "EntratiVaultCountResetDate" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -73,6 +74,7 @@ export interface IInventoryDatabase RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; NextRefill?: Date; Nemesis?: INemesisDatabase; + EntratiVaultCountResetDate?: Date; } export interface IQuestKeyDatabase { @@ -336,7 +338,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CalendarProgress: ICalendarProgress; SongChallenges?: ISongChallenge[]; EntratiVaultCountLastPeriod?: number; - EntratiVaultCountResetDate?: Date; + EntratiVaultCountResetDate?: IMongoDate; EntratiLabConquestUnlocked?: number; EntratiLabConquestHardModeStatus?: number; EntratiLabConquestCacheScoreMission?: number; From a77c1906bf0a51c5751261e619825d9534a6b577 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 23 Mar 2025 13:17:14 -0700 Subject: [PATCH 246/776] chore: add custom getAccountInfo endpoint (#1300) This will help the IRC server get all the information it needs for permission management in a single request. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1300 --- .../custom/getAccountInfoController.ts | 27 +++++++++++++++++++ src/routes/custom.ts | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 src/controllers/custom/getAccountInfoController.ts diff --git a/src/controllers/custom/getAccountInfoController.ts b/src/controllers/custom/getAccountInfoController.ts new file mode 100644 index 00000000..41e59a77 --- /dev/null +++ b/src/controllers/custom/getAccountInfoController.ts @@ -0,0 +1,27 @@ +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const getAccountInfoController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const info: IAccountInfo = { + DisplayName: account.DisplayName + }; + if (isAdministrator(account)) { + info.IsAdministrator = true; + } + const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); + if (guildMember) { + const guild = (await Guild.findOne({ _id: guildMember.guildId }, "Ranks"))!; + info.GuildId = guildMember.guildId.toString(); + info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions; + } + res.json(info); +}; + +interface IAccountInfo { + DisplayName: string; + IsAdministrator?: boolean; + GuildId?: string; + GuildPermissions?: number; +} diff --git a/src/routes/custom.ts b/src/routes/custom.ts index fa0f2225..20f19a59 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -6,6 +6,7 @@ import { pushArchonCrystalUpgradeController } from "@/src/controllers/custom/pus import { popArchonCrystalUpgradeController } from "@/src/controllers/custom/popArchonCrystalUpgradeController"; import { deleteAccountController } from "@/src/controllers/custom/deleteAccountController"; import { getNameController } from "@/src/controllers/custom/getNameController"; +import { getAccountInfoController } from "@/src/controllers/custom/getAccountInfoController"; import { renameAccountController } from "@/src/controllers/custom/renameAccountController"; import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController"; import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController"; @@ -29,6 +30,7 @@ customRouter.get("/pushArchonCrystalUpgrade", pushArchonCrystalUpgradeController customRouter.get("/popArchonCrystalUpgrade", popArchonCrystalUpgradeController); customRouter.get("/deleteAccount", deleteAccountController); customRouter.get("/getName", getNameController); +customRouter.get("/getAccountInfo", getAccountInfoController); customRouter.get("/renameAccount", renameAccountController); customRouter.get("/ircDropped", ircDroppedController); customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController); From 0085c20e11b9e3fbc712b22af77cacf7856cdaf6 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:33:26 -0700 Subject: [PATCH 247/776] feat(import): additional fields (#1305) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1305 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 34 +---- src/services/importService.ts | 153 ++++++++++++++++++- src/services/inventoryService.ts | 19 +-- src/types/inventoryTypes/inventoryTypes.ts | 25 +-- 4 files changed, 160 insertions(+), 71 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 198c68f7..a2e9b4b7 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -76,7 +76,6 @@ import { IIncentiveState, ISongChallenge, ILibraryPersonalProgress, - ICrewShipWeaponDatabase, IRecentVendorPurchaseDatabase, IVendorPurchaseHistoryEntryDatabase, IVendorPurchaseHistoryEntryClient, @@ -1118,25 +1117,6 @@ const alignmentSchema = new Schema( { _id: false } ); -const crewShipWeaponSchema2 = new Schema( - { - ItemType: String - }, - { id: false } -); - -crewShipWeaponSchema2.virtual("ItemId").get(function () { - return { $oid: this._id.toString() } satisfies IOid; -}); - -crewShipWeaponSchema2.set("toJSON", { - virtuals: true, - transform(_document, returnedObject) { - delete returnedObject._id; - delete returnedObject.__v; - } -}); - const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1259,20 +1239,20 @@ const inventorySchema = new Schema( //Default RailJack CrewShipAmmo: [typeCountSchema], - CrewShipWeapons: [crewShipWeaponSchema2], + CrewShipWeapons: [EquipmentSchema], CrewShipWeaponSkins: [upgradeSchema], + CrewShipSalvagedWeapons: [EquipmentSchema], + CrewShipSalvagedWeaponSkins: [upgradeSchema], - //NPC Crew and weapon + //RailJack Crew CrewMembers: [Schema.Types.Mixed], - CrewShipSalvagedWeaponSkins: [Schema.Types.Mixed], - CrewShipSalvagedWeapons: [Schema.Types.Mixed], //Complete Mission\Quests Missions: [missionSchema], QuestKeys: [questKeysSchema], ActiveQuest: { type: String, default: "" }, //item like DojoKey or Boss missions key - LevelKeys: [Schema.Types.Mixed], + LevelKeys: [typeCountSchema], //Active quests Quests: [Schema.Types.Mixed], @@ -1333,7 +1313,7 @@ const inventorySchema = new Schema( SpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined }, //New Quest Email - EmailItems: [TypeXPItemSchema], + EmailItems: [typeCountSchema], //Profile->Wishlist Wishlist: [String], @@ -1527,8 +1507,8 @@ export type InventoryDocumentProps = { WeaponSkins: Types.DocumentArray; QuestKeys: Types.DocumentArray; Drones: Types.DocumentArray; - CrewShipWeapons: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; + CrewShipSalvagedWeaponsSkins: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/services/importService.ts b/src/services/importService.ts index 535b67b7..ae16e86d 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -23,6 +23,12 @@ import { IKubrowPetDetailsDatabase, ILoadoutConfigClient, ILoadOutPresets, + INemesisClient, + INemesisDatabase, + IPendingRecipeClient, + IPendingRecipeDatabase, + IQuestKeyClient, + IQuestKeyDatabase, ISlots, IUpgradeClient, IUpgradeDatabase, @@ -144,6 +150,27 @@ const convertKubrowDetails = (client: IKubrowPetDetailsClient): IKubrowPetDetail }; }; +const convertQuestKey = (client: IQuestKeyClient): IQuestKeyDatabase => { + return { + ...client, + CompletionDate: convertOptionalDate(client.CompletionDate) + }; +}; + +const convertPendingRecipe = (client: IPendingRecipeClient): IPendingRecipeDatabase => { + return { + ...client, + CompletionDate: convertDate(client.CompletionDate) + }; +}; + +const convertNemesis = (client: INemesisClient): INemesisDatabase => { + return { + ...client, + d: convertDate(client.d) + }; +}; + export const importInventory = (db: TInventoryDatabaseDocument, client: Partial): void => { for (const key of equipmentKeys) { if (client[key] !== undefined) { @@ -153,10 +180,22 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< if (client.WeaponSkins !== undefined) { replaceArray(db.WeaponSkins, client.WeaponSkins.map(convertWeaponSkin)); } - if (client.Upgrades !== undefined) { - replaceArray(db.Upgrades, client.Upgrades.map(convertUpgrade)); + for (const key of ["Upgrades", "CrewShipSalvagedWeaponSkins", "CrewShipWeaponSkins"] as const) { + if (client[key] !== undefined) { + replaceArray(db[key], client[key].map(convertUpgrade)); + } } - for (const key of ["RawUpgrades", "MiscItems", "Consumables"] as const) { + for (const key of [ + "RawUpgrades", + "MiscItems", + "Consumables", + "Recipes", + "LevelKeys", + "EmailItems", + "ShipDecorations", + "CrewShipAmmo", + "CrewShipRawSalvage" + ] as const) { if (client[key] !== undefined) { db[key].splice(0, db[key].length); client[key].forEach(x => { @@ -190,8 +229,16 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< replaceSlots(db[key], client[key]); } } - if (client.UseAdultOperatorLoadout !== undefined) { - db.UseAdultOperatorLoadout = client.UseAdultOperatorLoadout; + for (const key of [ + "UseAdultOperatorLoadout", + "HasOwnedVoidProjectionsPreviously", + "ReceivedStartingGear", + "ArchwingEnabled", + "PlayedParkourTutorial" + ] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } } for (const key of [ "PlayerLevel", @@ -199,18 +246,37 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "PremiumCredits", "PremiumCreditsFree", "FusionPoints", - "PrimeTokens" + "PrimeTokens", + "TradesRemaining", + "GiftsRemaining", + "ChallengesFixVersion" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } - for (const key of ["ThemeStyle", "ThemeBackground", "ThemeSounds", "EquippedInstrument", "FocusAbility"] as const) { + for (const key of [ + "ThemeStyle", + "ThemeBackground", + "ThemeSounds", + "EquippedInstrument", + "FocusAbility", + "ActiveQuest", + "SupportedSyndicate", + "ActiveAvatarImageType" + ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } - for (const key of ["EquippedGear", "EquippedEmotes", "NodeIntrosCompleted"] as const) { + for (const key of [ + "EquippedGear", + "EquippedEmotes", + "NodeIntrosCompleted", + "DeathMarks", + "Wishlist", + "NemesisAbandonedRewards" + ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } @@ -242,6 +308,77 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< if (client.CustomMarkers !== undefined) { db.CustomMarkers = client.CustomMarkers; } + if (client.ChallengeProgress !== undefined) { + db.ChallengeProgress = client.ChallengeProgress; + } + if (client.QuestKeys !== undefined) { + replaceArray(db.QuestKeys, client.QuestKeys.map(convertQuestKey)); + } + if (client.LastRegionPlayed !== undefined) { + db.LastRegionPlayed = client.LastRegionPlayed; + } + if (client.PendingRecipes !== undefined) { + replaceArray(db.PendingRecipes, client.PendingRecipes.map(convertPendingRecipe)); + } + if (client.TauntHistory !== undefined) { + db.TauntHistory = client.TauntHistory; + } + if (client.LoreFragmentScans !== undefined) { + db.LoreFragmentScans = client.LoreFragmentScans; + } + for (const key of ["PendingSpectreLoadouts", "SpectreLoadouts"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.FocusXP !== undefined) { + db.FocusXP = client.FocusXP; + } + for (const key of ["Alignment", "AlignmentReplay"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.StepSequencers !== undefined) { + db.StepSequencers = client.StepSequencers; + } + if (client.CompletedJobChains !== undefined) { + db.CompletedJobChains = client.CompletedJobChains; + } + if (client.Nemesis !== undefined) { + db.Nemesis = convertNemesis(client.Nemesis); + } + if (client.PlayerSkills !== undefined) { + db.PlayerSkills = client.PlayerSkills; + } + if (client.LotusCustomization !== undefined) { + db.LotusCustomization = client.LotusCustomization; + } + if (client.CollectibleSeries !== undefined) { + db.CollectibleSeries = client.CollectibleSeries; + } + for (const key of ["LibraryAvailableDailyTaskInfo", "LibraryActiveDailyTaskInfo"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.EndlessXP !== undefined) { + db.EndlessXP = client.EndlessXP; + } + if (client.SongChallenges !== undefined) { + db.SongChallenges = client.SongChallenges; + } + if (client.Missions !== undefined) { + db.Missions = client.Missions; + } + if (client.FlavourItems !== undefined) { + db.FlavourItems.splice(0, db.FlavourItems.length); + client.FlavourItems.forEach(x => { + db.FlavourItems.push({ + ItemType: x.ItemType + }); + }); + } }; const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 74b64a44..b90d97df 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -25,8 +25,7 @@ import { ILibraryDailyTaskInfo, ICalendarProgress, IDroneClient, - IUpgradeClient, - ICrewShipWeaponClient + IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IMissionInventoryUpdateRequest, IUpdateChallengeProgressRequest } from "../types/requestTypes"; @@ -434,7 +433,7 @@ export const addItem = async ( } if (typeName in ExportRailjackWeapons) { return { - ...addCrewShipWeapon(inventory, typeName), + ...addEquipment(inventory, ExportRailjackWeapons[typeName].productCategory, typeName), ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) }; } @@ -947,20 +946,6 @@ export const addSkin = ( return inventoryChanges; }; -const addCrewShipWeapon = ( - inventory: TInventoryDatabaseDocument, - typeName: string, - inventoryChanges: IInventoryChanges = {} -): IInventoryChanges => { - const index = inventory.CrewShipWeapons.push({ ItemType: typeName, _id: new Types.ObjectId() }) - 1; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - inventoryChanges.CrewShipWeapons ??= []; - (inventoryChanges.CrewShipWeapons as ICrewShipWeaponClient[]).push( - inventory.CrewShipWeapons[index].toJSON() - ); - return inventoryChanges; -}; - const addCrewShipWeaponSkin = ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 54d50cc1..d4b99f6c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -30,9 +30,8 @@ export interface IInventoryDatabase | "Ships" | "WeaponSkins" | "Upgrades" - | "CrewShipSalvagedWeaponSkins" - | "CrewShipWeapons" | "CrewShipWeaponSkins" + | "CrewShipSalvagedWeaponSkins" | "AdultOperatorLoadOuts" | "OperatorLoadOuts" | "KahlLoadOuts" @@ -60,9 +59,8 @@ export interface IInventoryDatabase Ships: Types.ObjectId[]; WeaponSkins: IWeaponSkinDatabase[]; Upgrades: IUpgradeDatabase[]; - CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; - CrewShipWeapons: ICrewShipWeaponDatabase[]; CrewShipWeaponSkins: IUpgradeDatabase[]; + CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; AdultOperatorLoadOuts: IOperatorConfigDatabase[]; OperatorLoadOuts: IOperatorConfigDatabase[]; KahlLoadOuts: IOperatorConfigDatabase[]; @@ -114,7 +112,9 @@ export const equipmentKeys = [ "DataKnives", "MechSuits", "CrewShipHarnesses", - "KubrowPets" + "KubrowPets", + "CrewShipWeapons", + "CrewShipSalvagedWeapons" ] as const; export type TEquipmentKey = (typeof equipmentKeys)[number]; @@ -299,10 +299,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu PersonalTechProjects: IPersonalTechProject[]; PlayerSkills: IPlayerSkills; CrewShipAmmo: ITypeCount[]; - CrewShipSalvagedWeaponSkins: IUpgradeClient[]; - CrewShipWeapons: ICrewShipWeaponClient[]; - CrewShipSalvagedWeapons: IEquipmentClient[]; CrewShipWeaponSkins: IUpgradeClient[]; + CrewShipSalvagedWeaponSkins: IUpgradeClient[]; TradeBannedUntil?: IMongoDate; PlayedParkourTutorial: boolean; SubscribedToEmailsPersonalized: number; @@ -538,17 +536,6 @@ export interface ICrewShipWeapon { PORT_GUNS: ICrewShipPortGuns; } -// inventory.CrewShipWeapons -export interface ICrewShipWeaponClient { - ItemType: string; - ItemId: IOid; -} - -export interface ICrewShipWeaponDatabase { - ItemType: string; - _id: Types.ObjectId; -} - export interface ICrewShipPilotWeapon { PRIMARY_A: IEquipmentSelection; SECONDARY_A: IEquipmentSelection; From ac25ee51183deaeacb15bd1e1d8047867b0844ed Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Mar 2025 01:38:08 -0700 Subject: [PATCH 248/776] feat: redeemPromoCode (#1310) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1310 --- .../api/redeemPromoCodeController.ts | 34 +++ src/routes/api.ts | 2 + static/fixed_responses/glyphsCodes.json | 259 ++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 src/controllers/api/redeemPromoCodeController.ts create mode 100644 static/fixed_responses/glyphsCodes.json diff --git a/src/controllers/api/redeemPromoCodeController.ts b/src/controllers/api/redeemPromoCodeController.ts new file mode 100644 index 00000000..f0e615bc --- /dev/null +++ b/src/controllers/api/redeemPromoCodeController.ts @@ -0,0 +1,34 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { RequestHandler } from "express"; +import glyphCodes from "@/static/fixed_responses/glyphsCodes.json"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { addItem, getInventory } from "@/src/services/inventoryService"; + +export const redeemPromoCodeController: RequestHandler = async (req, res) => { + const body = getJSONfromString(String(req.body)); + if (!(body.codeId in glyphCodes)) { + res.status(400).send("INVALID_CODE").end(); + return; + } + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "FlavourItems"); + const acquiredGlyphs: string[] = []; + for (const glyph of (glyphCodes as Record)[body.codeId]) { + if (!inventory.FlavourItems.find(x => x.ItemType == glyph)) { + acquiredGlyphs.push(glyph); + await addItem(inventory, glyph); + } + } + if (acquiredGlyphs.length == 0) { + res.status(400).send("USED_CODE").end(); + return; + } + await inventory.save(); + res.json({ + FlavourItems: acquiredGlyphs + }); +}; + +interface IRedeemPromoCodeRequest { + codeId: string; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 9e573563..b6fd4316 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -77,6 +77,7 @@ import { playerSkillsController } from "@/src/controllers/api/playerSkillsContro import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; +import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; @@ -216,6 +217,7 @@ apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); +apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/saveDialogue.php", saveDialogueController); diff --git a/static/fixed_responses/glyphsCodes.json b/static/fixed_responses/glyphsCodes.json new file mode 100644 index 00000000..f1ad8a22 --- /dev/null +++ b/static/fixed_responses/glyphsCodes.json @@ -0,0 +1,259 @@ +{ + "1999-QUINCY": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImagePartyCDGlyph"], + "1999-VOICEPLAY": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageBigBytesPizzaGlyph"], + "6IXGATSU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSixixgatsu"], + "ADMIRALBAHROO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAdmiralBahroo"], + "AEONKNIGHT86": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAeonKnight"], + "AGAYGUYPLAYS": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorAGGP"], + "AKARI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAkariayataka"], + "ALAINLOVEGLYPH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAlainLove"], + "ALEXANDERDARIO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAlexanderDario"], + "AMPROV": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGoku"], + "ANGRYUNICORN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAngryUnicorn"], + "ANJETCAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAnJetCat"], + "ANNOYINGKILLAH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAnnoyingKillah"], + "ARGONSIX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageArgonSix"], + "ASHISOGITENNO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAshisogiTenno"], + "ASURATENSHI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTenshi"], + "AUNTIETAN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFromThe70s"], + "AVELNA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAvelna"], + "AZNITROUS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAznitrous"], + "BIGJIMID": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBigJimID"], + "BLACKONI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBlackOni"], + "BLAZINGCOBALT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBlazingCobalt"], + "BLUEBERRYCAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBlueberryCat"], + "BRAZILCOMMUNITYDISCORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBRCommunityDiscord"], + "BRICKY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBricky"], + "BROTHERDAZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOldDirtyDaz"], + "BROZIME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBrozime"], + "BUFF00N": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBuff00n"], + "BURNBXX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBurnBxx"], + "BWANA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBwana"], + "CALAMITYDEATH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCalamityDeath"], + "CALEYEMERALD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCaleyEmerald"], + "CANOFCRAIG": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCanOfCraig"], + "CARCHARA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCarchara"], + "CASARDIS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCasardis"], + "CEPHALONSQUARED": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCephalonSquared"], + "CGSKNACKIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCGsKnackie"], + "CHACYTAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageChacytay"], + "CHAR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageChar"], + "CHELESTRA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageChelestra"], + "CLEONATURIN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCleoNaturin"], + "CODOMA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCodoma"], + "COHHCARNAGE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCohhCarnage"], + "COLDSCAR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageColdScar"], + "COLDTIGER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageColdTiger"], + "CONCLAVEDISCORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageConclaveDiscord"], + "CONFUSEDWARFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageConfusedWarframe"], + "CONQUERA2024": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageConqueraGlyphVI", "/Lotus/Types/StoreItems/AvatarImages/AvatarImageConqueraGlyphVII"], + "COPYKAVAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCopyKavat"], + "CPT_KIMGLYPH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCptKim"], + "CROWDI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCrowdi"], + "DAIDAIKIRI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDaiDaiKiri"], + "DANIELTHEDEMON": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorDanieltheDemon"], + "DANILY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDanily"], + "DARIKAART": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDarikaArt"], + "DASTERCREATIONS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDasterCreations"], + "DATLOON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDatLoon"], + "DAYJOBO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDayJoBo"], + "DEATHMAGGOT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagedeathma666ot"], + "DEBBYSHEEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDebbysheen"], + "DEEJAYKNIGHT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDeejayKnight"], + "DEEPBLUEBEARD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDeepBlueBeard"], + "DESTROHIDO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDestrohido"], + "DEUCETHEGAMER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDeuceTheGamer"], + "DILLYFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDillyFrame"], + "DIMITRIV2": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDimitriVTwo"], + "DISFUSIONAL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDisfusional"], + "DJTECHLIVE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDJTechlive"], + "DKDIAMANTES": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorDKDiamantes"], + "DNEXUS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDNexus"], + "EDRICK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEdrick"], + "EDUIY16": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEduiy"], + "ELDANKER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageD4NK3R"], + "ELGRINEEREXILIADO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageElGrineerExiliado"], + "ELICEGAMEPLAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEliceGameplay"], + "ELNORAELEO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageElNoraEleo"], + "EMOVJ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEmovj"], + "EMPYREANCAP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEmpyreanCap"], + "ENDOTTI_": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEndotti"], + "ETERION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEterion"], + "EXTRACREDITS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageExtraCredits"], + "FACELESSBEANIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFacelessBeanie"], + "FASHIONFRAMEISENDGAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFashionFrameIsEndgame"], + "FATED2PERISH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFated2Perish"], + "FEELLIKEAPLAYER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFeelLikeAPlayer"], + "FERREUSDEMON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFerreusDemon"], + "FINLAENA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFinlaena"], + "FLOOFYDWAGON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFloofyDwagon"], + "FR4G-TP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFR4GTP"], + "FROSTYNOVAPRIME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFrostyNovaPrime"], + "FROZENBAWZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFrozenbawz"], + "GARA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGara"], + "GERMANCOMMUNITYDISCORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGermanCommunityDiscord"], + "GINGY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGingy"], + "GLAMSHATTERSKULL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGlamShatterskull"], + "GRINDHARDSQUAD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGrindHardSquad"], + "H3DSH0T": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorH3dsh0t"], + "HAPPINESSDARK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHappinessDark"], + "HOKUPROPS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHokuProps"], + "HOMIINVOCADO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHomiInvocado"], + "HOTSHOMSTORIES": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHotsHomStories"], + "HYDROXATE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHydroxate"], + "IFLYNN": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorIflynn"], + "IKEDO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageIkedo"], + "IM7HECLOWN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageIm7heClown"], + "INEXPENSIVEGAMER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageInexpensiveGamer"], + "INFERNOTHEFIRELORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageInfernoTheFirelord"], + "INFODIVERSAO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageInfodiversao"], + "ITSJUSTTOE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageToxickToe"], + "IWOPLY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageIwoply"], + "JAMIEVOICEOVER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJamieVoiceOver"], + "JESSITHROWER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJessiThrower"], + "JOEYZERO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJoeyZero"], + "JORIALE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJoriale"], + "JUSTHAILEY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJustHailey"], + "JUSTRLC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRLCGaming"], + "K1LLERBARBIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKillerBarbie"], + "KAVATSSCHROEDINGER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKavatsSchroedinger"], + "KENSHINWF": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKenshinWF"], + "KINGGOTHALION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKingGothalion"], + "KIRARAHIME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKirarahime"], + "KIRDY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKirdy"], + "KIWAD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKiwad"], + "KR1PTONPLAYER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKr1ptonPlayer"], + "KRETDUY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKretduy"], + "KYAII": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagekyaii"], + "L1FEWATER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLifewater"], + "LADYNOVITA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLadyNovita"], + "LADYTHELADDY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLadyTheLaddy"], + "LEODOODLING": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLeoDoodling"], + "LEYZARGAMINGVIEWS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLeyzarViewGaming"], + "LIGHTMICKE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLightmicke"], + "LIGHTNINGCOSPLAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLightningCosplay"], + "LILLEXI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLilLexi"], + "LUCIANPLAYSALLDAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLucianPlaysAllDay"], + "LYNXARIA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLynxaria"], + "MACHO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLokKingMacho"], + "MADFURY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHypercaptai"], + "MAKARIMORPH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMakarimorph"], + "MAOMIX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMaomix"], + "MCGAMERCZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMCGamerCZ"], + "MCIK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMCIK"], + "MCMONKEYS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMCMonkeys"], + "MECORE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMeCore"], + "MEDUSACAPTURES": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMedusaCaptures"], + "MHBLACKY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMHBlacky"], + "MICHELPOSTMA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTheNextLevel"], + "MIKETHEBARD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTVSBOH"], + "MISSFWUFFY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMissFwuffy"], + "MISTERGAMER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTennoForever"], + "MJIKTHIZE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMjikThize"], + "MOGAMU": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorMogamu"], + "MOVEMBER2024": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageMovember"], + "MRROADBLOCK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMrRoadBlock"], + "MRSTEELWAR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMrSteelWar"], + "MRWARFRAMEGUY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMrWarframeGuy"], + "N00BLSHOWTEK": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorN00blShowtek"], + "NELOSART": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageNelosart"], + "NOMNOM": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageNononom"], + "NOSYMPATHYY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageNoSympathyy"], + "NP161": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagenponesixtyone"], + "ODDIEOWL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOddieowl"], + "OOSIJ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOOSIJ"], + "ORIGINALWICKEDFUN": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorOriginalWickedfun"], + "ORPHEUSDELUXE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOrpheusDeluxe"], + "OTTOFYRE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOttofyre"], + "OZKU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOzku"], + "PAMMYJAMMY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePammyJammy"], + "PANDAAHH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePandaahhhhh"], + "PAPATLION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePapaTLion"], + "PHONGFU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePhongFu"], + "PLAGUEDIRECTOR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePlagueDirector"], + "PLEXICOSPLAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePlexiCosplay"], + "POKKETNINJA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePokketNinja"], + "POSTITV": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePostiTV"], + "PRIDE2024": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImagePrideGlyph"], + "PRIMEDAVERAGE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePrimedAverage"], + "PROFESSORBROMAN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageProfessorBroman"], + "PURKINJE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePurkinje"], + "PURPLEFLURP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePurpleFlurp"], + "PYRAH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePyrah"], + "PYRRHICSERENITY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePyrrhicSerenity"], + "QUADLYSTOP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageQuadlyStop"], + "R/WARFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageReddit"], + "RAGINGTERROR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRagingTerror"], + "RAHETALIUS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRahetalius"], + "RAHNY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRahny"], + "RAINBOWWAFFLES": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRainbowWaffles"], + "RELENTLESSZEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRelentlessZen"], + "RETROALCHEMIST": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRetroAlchemist"], + "REYGANSO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageReyGanso"], + "RIKENZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRIKENZ"], + "RIPPZ0R": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRippz0r"], + "RITENS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRitens"], + "ROYALPRAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRoyalPrat"], + "RUSTYFIN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRustyFin"], + "SAPMATIC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSapmatic"], + "SARAHTSANG": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSarahTsang"], + "SCALLION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageScallion"], + "SCARLETMOON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageScarletMoon"], + "SEARYN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSearyn"], + "SERDARSARI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBBSChainWarden"], + "SHARLAZARD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSharlazard"], + "SHENZHAO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageShenzhao"], + "SHERPA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSherpaRage"], + "SHUL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageShulGaming"], + "SIEJOUMBRA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSiejoUmbra"], + "SILENTMASHIKO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSilentMashiko"], + "SILLFIX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSillfix"], + "SILVERVALE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSilvervale"], + "SKILLUP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSkillUp"], + "SMOODIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSmoodie"], + "SN0WRC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSn0wRC"], + "SPACEWAIFU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSpaceWaifu"], + "SPANDY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSpandy"], + "STR8OPTICROYAL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageStr8opticroyal"], + "STRIPPIN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageStrippin"], + "STUDIOCYEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageStudioCyen"], + "TACTICALPOTATO": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorTacticalPotato"], + "TANCHAN": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorTanchan"], + "TBGKARU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTBGKaru"], + "TEAWREX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTeawrex"], + "THEGAMIO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTheGamio"], + "THEKENGINEER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKengineer"], + "THEPANDA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageThePandaNEight"], + "TINBEARS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTinBears"], + "TIOMARIO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTioMario"], + "TIORAMON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTioRamon"], + "TORTOISE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWDTortoise"], + "TOTALN3WB": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDayTotalN3wb"], + "TRASHFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTrashFrame"], + "TRIBUROS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTriburos"], + "TWILA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTwila"], + "UNREALYUKI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageUnrealYuki"], + "UREIFEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageUreiFen"], + "VAMP6X6X6X": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWarframeMadness"], + "VAMPPIRE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVamppire"], + "VARLINATOR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVarlinator"], + "VASHCOWAII": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVashCowaii"], + "VASHKA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVashka"], + "VERNOC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVernoc"], + "VOIDFISSUREBR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVoidFissureBR"], + "VOLI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVoli"], + "VOLTTHEHERO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVoltTheHero"], + "VVHITEANGEL": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorVVhiteAngel"], + "WALTERDV": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWalterDV"], + "WANDERBOTS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWanderbots"], + "WARFRAMEFLO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWarframeFlo"], + "WEALWEST": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWealWest"], + "WIDESCREENJOHN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWidescreenJohn"], + "WOXLI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWoxli"], + "XBOCCHANVTX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBocchanVT"], + "XENOGELION": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorXenogelion"], + "XXVAMPIXX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageXxVampixx"], + "YOURLUCKYCLOVER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageYourLuckyClover"], + "ZARIONIS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageZarionis"], + "ZXPFER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageZxpfer"] +} From 3e2e73f6eb02cf8273fe57652bf34a8f9f388cbf Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Mar 2025 01:38:32 -0700 Subject: [PATCH 249/776] feat: handle Boosters in missionInventoryUpdate (#1311) Closes #751 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1311 --- src/services/missionInventoryUpdateService.ts | 6 ++++++ src/types/inventoryTypes/inventoryTypes.ts | 1 + src/types/requestTypes.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 44d0fdeb..86561ed2 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -11,6 +11,7 @@ import { logger } from "@/src/utils/logger"; import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { + addBooster, addChallenges, addConsumables, addCrewShipAmmo, @@ -284,6 +285,11 @@ export const addMissionInventoryUpdates = async ( upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress }); break; + case "Boosters": + value.forEach(booster => { + addBooster(booster.ItemType, booster.ExpiryDate, inventory); + }); + break; case "SyndicateId": { inventory.CompletedSyndicates.push(value); break; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d4b99f6c..7e18be4d 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -365,6 +365,7 @@ export interface IAlignment { export interface IBooster { ExpiryDate: number; ItemType: string; + UsesRemaining?: number; } export interface IChallengeInstanceState { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 78e43c5c..7ca33a84 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -104,6 +104,7 @@ export type IMissionInventoryUpdateRequest = { }[]; DeathMarks?: string[]; Nemesis?: number; + Boosters?: IBooster[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From e65393f43303d29cb7a5d1f5f370919fdc989585 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Mar 2025 11:31:53 -0700 Subject: [PATCH 250/776] chore: use json-with-bigint for JSON.stringify hook (#1312) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1312 --- package-lock.json | 8 ++++---- package.json | 2 +- src/index.ts | 23 +++++------------------ 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7fbf5002..97ecfb8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", - "json-with-bigint": "^3.2.1", + "json-with-bigint": "^3.2.2", "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", @@ -2348,9 +2348,9 @@ "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.1.tgz", - "integrity": "sha512-0f8RHpU1AwBFwIPmtm71W+cFxzlXdiBmzc3JqydsNDSKSAsr0Lso6KXRbz0h2LRwTIRiHAk/UaD+xaAN5f577w==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.2.tgz", + "integrity": "sha512-zbaZ+MZ2PEcAD0yINpxvlLMKzoC1GPqy5p8/ZgzRJRoB+NCczGrTX9x2ashSvkfYTitQKbV5aYQCJCiHxrzF2w==", "license": "MIT" }, "node_modules/json5": { diff --git a/package.json b/package.json index 0cd17cb3..98e770ec 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", - "json-with-bigint": "^3.2.1", + "json-with-bigint": "^3.2.2", "mongoose": "^8.11.0", "morgan": "^1.10.0", "typescript": ">=5.5 <5.6.0", diff --git a/src/index.ts b/src/index.ts index f540f96e..8c38237f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,25 +9,12 @@ import { app } from "./app"; import { config, validateConfig } from "./services/configService"; import { registerLogFileCreationListener } from "@/src/utils/logger"; import mongoose from "mongoose"; +import { Json, JSONStringify } from "json-with-bigint"; -// Patch JSON.stringify to work flawlessly with Bigints. Yeah, it's not pretty. -// TODO: Might wanna use json-with-bigint if/when possible. -{ - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - (BigInt.prototype as any).toJSON = function (): string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - return "" + this.toString() + ""; - }; - const og_stringify = JSON.stringify; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - JSON.stringify = (obj: any, replacer?: any, space?: string | number): string => { - return og_stringify(obj, replacer as string[], space) - .split(`"`) - .join(``) - .split(`"`) - .join(``); - }; -} +// Patch JSON.stringify to work flawlessly with Bigints. +JSON.stringify = (obj: Exclude, _replacer?: unknown, space?: string | number): string => { + return JSONStringify(obj, space); +}; registerLogFileCreationListener(); validateConfig(); From 4afc8bc8c6967ddc5fac1fc067d5dd256c0cf374 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Mar 2025 11:32:01 -0700 Subject: [PATCH 251/776] chore: use inventory projection for updateChallengeProgress (#1313) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1313 --- .../api/updateChallengeProgressController.ts | 19 +++++++++++++------ src/services/inventoryService.ts | 14 +------------- src/types/requestTypes.ts | 6 ------ 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index 2889a333..a69e995b 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -1,16 +1,23 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { updateChallengeProgress } from "@/src/services/inventoryService"; -import { IUpdateChallengeProgressRequest } from "@/src/types/requestTypes"; +import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService"; +import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; -const updateChallengeProgressController: RequestHandler = async (req, res) => { - const payload = getJSONfromString(String(req.body)); +export const updateChallengeProgressController: RequestHandler = async (req, res) => { + const challenges = getJSONfromString(String(req.body)); const accountId = await getAccountIdForRequest(req); - await updateChallengeProgress(payload, accountId); + const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory"); + addChallenges(inventory, challenges.ChallengeProgress); + addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); + await inventory.save(); res.status(200).end(); }; -export { updateChallengeProgressController }; +interface IUpdateChallengeProgressRequest { + ChallengeProgress: IChallengeProgress[]; + SeasonChallengeHistory: ISeasonChallenge[]; + SeasonChallengeCompletions: ISeasonChallenge[]; +} diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index b90d97df..dde254f1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -28,7 +28,7 @@ import { IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; -import { IMissionInventoryUpdateRequest, IUpdateChallengeProgressRequest } from "../types/requestTypes"; +import { IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { convertInboxMessage, fromStoreItem, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; import { @@ -1205,18 +1205,6 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; }; -export const updateChallengeProgress = async ( - challenges: IUpdateChallengeProgressRequest, - accountId: string -): Promise => { - const inventory = await getInventory(accountId); - - addChallenges(inventory, challenges.ChallengeProgress); - addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); - - await inventory.save(); -}; - export const addSeasonalChallengeHistory = ( inventory: TInventoryDatabaseDocument, itemsArray: ISeasonChallenge[] | undefined diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 7ca33a84..9b977d78 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -25,12 +25,6 @@ export interface IAffiliationChange { Title: number; } -export interface IUpdateChallengeProgressRequest { - ChallengeProgress: IChallengeProgress[]; - SeasonChallengeHistory: ISeasonChallenge[]; - SeasonChallengeCompletions: ISeasonChallenge[]; -} - export type IMissionInventoryUpdateRequest = { MiscItems?: ITypeCount[]; Recipes?: ITypeCount[]; From 2ec2b0278ab35404c2a34bf56ebd6b2546cbc897 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 24 Mar 2025 11:32:08 -0700 Subject: [PATCH 252/776] chore: use model.findById where possible (#1315) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1315 --- src/controllers/api/addToGuildController.ts | 2 +- src/controllers/api/confirmGuildInvitationController.ts | 2 +- src/controllers/api/contributeGuildClassController.ts | 2 +- src/controllers/api/getGuildController.ts | 2 +- src/controllers/api/getGuildDojoController.ts | 2 +- src/controllers/api/getGuildLogController.ts | 2 +- src/controllers/api/logoutController.ts | 2 +- src/controllers/api/removeFromGuildController.ts | 2 +- src/controllers/api/setGuildMotdController.ts | 2 +- src/controllers/custom/getAccountInfoController.ts | 2 +- src/controllers/dynamic/getProfileViewingDataController.ts | 6 +++--- src/services/guildService.ts | 2 +- src/services/inboxService.ts | 2 +- src/services/shipService.ts | 2 +- 14 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index 0d24cd00..ee1c0fc4 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -18,7 +18,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { return; } - const guild = (await Guild.findOne({ _id: payload.GuildId.$oid }, "Name"))!; + const guild = (await Guild.findById(payload.GuildId.$oid, "Name"))!; const senderAccount = await getAccountForRequest(req); if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { res.status(400).json("Invalid permission"); diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts index 7331af03..2685be99 100644 --- a/src/controllers/api/confirmGuildInvitationController.ts +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -19,7 +19,7 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res) new Types.ObjectId(req.query.clanId as string) ); - const guild = (await Guild.findOne({ _id: req.query.clanId as string }))!; + const guild = (await Guild.findById(req.query.clanId as string))!; guild.RosterActivity ??= []; guild.RosterActivity.push({ diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index 629ec2ff..854f5223 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -11,7 +11,7 @@ import { Types } from "mongoose"; export const contributeGuildClassController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const payload = getJSONfromString(String(req.body)); - const guild = (await Guild.findOne({ _id: payload.GuildId }))!; + const guild = (await Guild.findById(payload.GuildId))!; // First contributor initiates ceremony and locks the pending class. if (!guild.CeremonyContributors) { diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 37a9ed26..8a803bfe 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -9,7 +9,7 @@ const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { - const guild = await Guild.findOne({ _id: inventory.GuildId }); + const guild = await Guild.findById(inventory.GuildId); if (guild) { // Handle guilds created before we added discriminators if (guild.Name.indexOf("#") == -1) { diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index d03252d8..3c48a4f4 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -6,7 +6,7 @@ import { getDojoClient } from "@/src/services/guildService"; export const getGuildDojoController: RequestHandler = async (req, res) => { const guildId = req.query.guildId as string; - const guild = await Guild.findOne({ _id: guildId }); + const guild = await Guild.findById(guildId); if (!guild) { res.status(404).end(); return; diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index a0386e76..037d07be 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -9,7 +9,7 @@ export const getGuildLogController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { - const guild = await Guild.findOne({ _id: inventory.GuildId }); + const guild = await Guild.findById(inventory.GuildId); if (guild) { const log: Record = { RoomChanges: [], diff --git a/src/controllers/api/logoutController.ts b/src/controllers/api/logoutController.ts index 735014d4..f6f8863f 100644 --- a/src/controllers/api/logoutController.ts +++ b/src/controllers/api/logoutController.ts @@ -4,7 +4,7 @@ import { Account } from "@/src/models/loginModel"; const logoutController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const account = await Account.findOne({ _id: accountId }); + const account = await Account.findById(accountId); if (account) { account.Nonce = 0; await account.save(); diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 3a794634..352c58d3 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -42,7 +42,7 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { guild.RosterActivity ??= []; if (isKick) { - const kickee = (await Account.findOne({ _id: payload.userId }))!; + const kickee = (await Account.findById(payload.userId))!; guild.RosterActivity.push({ dateTime: new Date(), entryType: 12, diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts index e0eb2aac..374ae0f1 100644 --- a/src/controllers/api/setGuildMotdController.ts +++ b/src/controllers/api/setGuildMotdController.ts @@ -8,7 +8,7 @@ import { RequestHandler } from "express"; export const setGuildMotdController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); const inventory = await getInventory(account._id.toString(), "GuildId"); - const guild = (await Guild.findOne({ _id: inventory.GuildId! }))!; + const guild = (await Guild.findById(inventory.GuildId!))!; if (!(await hasGuildPermission(guild, account._id, GuildPermission.Herald))) { res.status(400).json("Invalid permission"); return; diff --git a/src/controllers/custom/getAccountInfoController.ts b/src/controllers/custom/getAccountInfoController.ts index 41e59a77..5d83b56b 100644 --- a/src/controllers/custom/getAccountInfoController.ts +++ b/src/controllers/custom/getAccountInfoController.ts @@ -12,7 +12,7 @@ export const getAccountInfoController: RequestHandler = async (req, res) => { } const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); if (guildMember) { - const guild = (await Guild.findOne({ _id: guildMember.guildId }, "Ranks"))!; + const guild = (await Guild.findById(guildMember.guildId, "Ranks"))!; info.GuildId = guildMember.guildId.toString(); info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions; } diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts index 9e9c764f..b02189d0 100644 --- a/src/controllers/dynamic/getProfileViewingDataController.ts +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -26,13 +26,13 @@ export const getProfileViewingDataController: RequestHandler = async (req, res) res.status(400).end(); return; } - const account = await Account.findOne({ _id: req.query.playerId as string }, "DisplayName"); + const account = await Account.findById(req.query.playerId as string, "DisplayName"); if (!account) { res.status(400).send("No account or guild ID specified"); return; } const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; - const loadout = (await Loadout.findOne({ _id: inventory.LoadOutPresets }, "NORMAL"))!; + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; const result: IPlayerProfileViewingDataResult = { AccountId: toOid(account._id), @@ -88,7 +88,7 @@ export const getProfileViewingDataController: RequestHandler = async (req, res) } } if (inventory.GuildId) { - const guild = (await Guild.findOne({ _id: inventory.GuildId }, "Name Tier XP Class"))!; + const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class"))!; result.GuildId = toOid(inventory.GuildId); result.GuildName = guild.Name; result.GuildTier = guild.Tier; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 5b00b622..0237cd2b 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -37,7 +37,7 @@ export const getGuildForRequestEx = async ( if (!inventory.GuildId || inventory.GuildId.toString() != guildId) { throw new Error("Account is not in the guild that it has sent a request for"); } - const guild = await Guild.findOne({ _id: guildId }); + const guild = await Guild.findById(guildId); if (!guild) { throw new Error("Account thinks it is in a guild that doesn't exist"); } diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index fa7a3812..49b5cca3 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -11,7 +11,7 @@ export const getAllMessagesSorted = async (accountId: string): Promise> => { - const message = await Inbox.findOne({ _id: messageId }); + const message = await Inbox.findById(messageId); if (!message) { throw new Error(`Message not found ${messageId}`); diff --git a/src/services/shipService.ts b/src/services/shipService.ts index cc97d52d..7552fb97 100644 --- a/src/services/shipService.ts +++ b/src/services/shipService.ts @@ -21,7 +21,7 @@ export const createShip = async ( }; export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = ""): Promise => { - const ship = await Ship.findOne({ _id: shipId }, fieldSelection); + const ship = await Ship.findById(shipId, fieldSelection); if (!ship) { throw new Error(`error finding a ship with id ${shipId.toString()}`); From a12e5968da20650a4e3e86dc630429a92b1cf774 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 03:25:58 -0700 Subject: [PATCH 253/776] feat: race leaderboards (#1314) Initial leaderboard system. Currently only tracking races, tho. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1314 --- .../custom/deleteAccountController.ts | 2 + .../stats/leaderboardController.ts | 19 +++++ src/models/leaderboardModel.ts | 26 ++++++ src/routes/stats.ts | 7 +- src/services/leaderboardService.ts | 84 +++++++++++++++++++ src/services/statsService.ts | 25 ++++++ src/types/leaderboardTypes.ts | 17 ++++ 7 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/controllers/stats/leaderboardController.ts create mode 100644 src/models/leaderboardModel.ts create mode 100644 src/services/leaderboardService.ts create mode 100644 src/types/leaderboardTypes.ts diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index 63ade312..e5c466b4 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -8,6 +8,7 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { Ship } from "@/src/models/shipModel"; import { Stats } from "@/src/models/statsModel"; import { GuildMember } from "@/src/models/guildModel"; +import { Leaderboard } from "@/src/models/leaderboardModel"; export const deleteAccountController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -17,6 +18,7 @@ export const deleteAccountController: RequestHandler = async (req, res) => { GuildMember.deleteMany({ accountId: accountId }), Inbox.deleteMany({ ownerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }), + Leaderboard.deleteMany({ ownerId: accountId }), Loadout.deleteOne({ loadoutOwnerId: accountId }), PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }), Ship.deleteMany({ ShipOwnerId: accountId }), diff --git a/src/controllers/stats/leaderboardController.ts b/src/controllers/stats/leaderboardController.ts new file mode 100644 index 00000000..b76e5e16 --- /dev/null +++ b/src/controllers/stats/leaderboardController.ts @@ -0,0 +1,19 @@ +import { getLeaderboard } from "@/src/services/leaderboardService"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const leaderboardController: RequestHandler = async (req, res) => { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + const payload = JSON.parse(String(req.body)) as ILeaderboardRequest; + res.json({ + results: await getLeaderboard(payload.field, payload.before, payload.after, payload.guildId, payload.pivotId) + }); +}; + +interface ILeaderboardRequest { + field: string; + before: number; + after: number; + guildId?: string; + pivotId?: string; +} diff --git a/src/models/leaderboardModel.ts b/src/models/leaderboardModel.ts new file mode 100644 index 00000000..5de2f608 --- /dev/null +++ b/src/models/leaderboardModel.ts @@ -0,0 +1,26 @@ +import { Document, model, Schema, Types } from "mongoose"; +import { ILeaderboardEntryDatabase } from "../types/leaderboardTypes"; + +const leaderboardEntrySchema = new Schema( + { + leaderboard: { type: String, required: true }, + ownerId: { type: Schema.Types.ObjectId, required: true }, + displayName: { type: String, required: true }, + score: { type: Number, required: true }, + guildId: Schema.Types.ObjectId, + expiry: { type: Date, required: true } + }, + { id: false } +); + +leaderboardEntrySchema.index({ leaderboard: 1 }); +leaderboardEntrySchema.index({ leaderboard: 1, ownerId: 1 }, { unique: true }); +leaderboardEntrySchema.index({ expiry: 1 }, { expireAfterSeconds: 0 }); // With this, MongoDB will automatically delete expired entries. + +export const Leaderboard = model("Leaderboard", leaderboardEntrySchema); + +// eslint-disable-next-line @typescript-eslint/ban-types +export type TLeaderboardEntryDocument = Document & { + _id: Types.ObjectId; + __v: number; +} & ILeaderboardEntryDatabase; diff --git a/src/routes/stats.ts b/src/routes/stats.ts index 59290675..11705259 100644 --- a/src/routes/stats.ts +++ b/src/routes/stats.ts @@ -1,11 +1,12 @@ -import { viewController } from "../controllers/stats/viewController"; -import { uploadController } from "@/src/controllers/stats/uploadController"; - import express from "express"; +import { viewController } from "@/src/controllers/stats/viewController"; +import { uploadController } from "@/src/controllers/stats/uploadController"; +import { leaderboardController } from "@/src/controllers/stats/leaderboardController"; const statsRouter = express.Router(); statsRouter.get("/view.php", viewController); statsRouter.post("/upload.php", uploadController); +statsRouter.post("/leaderboardWeekly.php", leaderboardController); export { statsRouter }; diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts new file mode 100644 index 00000000..afbe1623 --- /dev/null +++ b/src/services/leaderboardService.ts @@ -0,0 +1,84 @@ +import { Leaderboard, TLeaderboardEntryDocument } from "../models/leaderboardModel"; +import { ILeaderboardEntryClient } from "../types/leaderboardTypes"; + +export const submitLeaderboardScore = async ( + leaderboard: string, + ownerId: string, + displayName: string, + score: number, + guildId?: string +): Promise => { + const schedule = leaderboard.split(".")[0] as "daily" | "weekly"; + let expiry: Date; + if (schedule == "daily") { + expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); + } else { + const EPOCH = 1734307200 * 1000; // Monday + const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + expiry = new Date(weekEnd); + } + await Leaderboard.findOneAndUpdate( + { leaderboard, ownerId }, + { $max: { score }, $set: { displayName, guildId, expiry } }, + { upsert: true } + ); +}; + +export const getLeaderboard = async ( + leaderboard: string, + before: number, + after: number, + guildId?: string, + pivotId?: string +): Promise => { + const filter: { leaderboard: string; guildId?: string } = { leaderboard }; + if (guildId) { + filter.guildId = guildId; + } + + let entries: TLeaderboardEntryDocument[]; + let r: number; + if (pivotId) { + const pivotDoc = await Leaderboard.findOne({ ...filter, ownerId: pivotId }); + if (!pivotDoc) { + return []; + } + const beforeDocs = await Leaderboard.find({ + ...filter, + score: { $gt: pivotDoc.score } + }) + .sort({ score: 1 }) + .limit(before); + const afterDocs = await Leaderboard.find({ + ...filter, + score: { $lt: pivotDoc.score } + }) + .sort({ score: -1 }) + .limit(after); + entries = [...beforeDocs.reverse(), pivotDoc, ...afterDocs]; + r = + (await Leaderboard.countDocuments({ + ...filter, + score: { $gt: pivotDoc.score } + })) - beforeDocs.length; + } else { + entries = await Leaderboard.find(filter) + .sort({ score: -1 }) + .skip(before) + .limit(after - before); + r = before; + } + const res: ILeaderboardEntryClient[] = []; + for (const entry of entries) { + res.push({ + _id: entry.ownerId.toString(), + s: entry.score, + r: ++r, + n: entry.displayName + }); + } + return res; +}; diff --git a/src/services/statsService.ts b/src/services/statsService.ts index ef467723..a6ca826c 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -11,6 +11,7 @@ import { } from "@/src/types/statTypes"; import { logger } from "@/src/utils/logger"; import { addEmailItem, getInventory } from "@/src/services/inventoryService"; +import { submitLeaderboardScore } from "./leaderboardService"; export const createStats = async (accountId: string): Promise => { const stats = new Stats({ accountOwnerId: accountId }); @@ -301,6 +302,13 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) } else { playerStats.Races.set(race, { highScore }); } + + await submitLeaderboardScore( + "daily.accounts." + race, + accountOwnerId, + payload.displayName, + highScore + ); } break; @@ -308,9 +316,20 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) case "ZephyrScore": case "SentinelGameScore": case "CaliberChicksScore": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data as number; + break; + case "DojoObstacleScore": playerStats[category] ??= 0; if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "weekly.accounts." + category, + accountOwnerId, + payload.displayName, + data as number, + payload.guildId + ); break; case "OlliesCrashCourseScore": @@ -330,6 +349,12 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) ); } if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "weekly.accounts." + category, + accountOwnerId, + payload.displayName, + data as number + ); break; default: diff --git a/src/types/leaderboardTypes.ts b/src/types/leaderboardTypes.ts new file mode 100644 index 00000000..5173a3a3 --- /dev/null +++ b/src/types/leaderboardTypes.ts @@ -0,0 +1,17 @@ +import { Types } from "mongoose"; + +export interface ILeaderboardEntryDatabase { + leaderboard: string; + ownerId: Types.ObjectId; + displayName: string; + score: number; + guildId?: Types.ObjectId; + expiry: Date; +} + +export interface ILeaderboardEntryClient { + _id: string; // owner id + s: number; // score + r: number; // rank + n: string; // displayName +} From 3ba58114b91434a8cf1463cd6d7634701c224ac2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 03:26:18 -0700 Subject: [PATCH 254/776] fix: ignore parts without premiumPrice when generating daily special (#1316) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1316 --- src/controllers/api/modularWeaponSaleController.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index 46c4bec5..3e07c1d6 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -21,7 +21,7 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const modularWeaponSaleController: RequestHandler = async (req, res) => { const partTypeToParts: Record = {}; for (const [uniqueName, data] of Object.entries(ExportWeapons)) { - if (data.partType) { + if (data.partType && data.premiumPrice) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition partTypeToParts[data.partType] ??= []; partTypeToParts[data.partType].push(uniqueName); @@ -147,11 +147,7 @@ const getModularWeaponSale = ( const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])); let partsCost = 0; for (const part of parts) { - const meta = ExportWeapons[part]; - if (!meta.premiumPrice) { - throw new Error(`no premium price for ${part}`); - } - partsCost += meta.premiumPrice; + partsCost += ExportWeapons[part].premiumPrice!; } return { Name: name, From 31c1fc245f5190431c90fa962565b998527da028 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 03:26:32 -0700 Subject: [PATCH 255/776] fix: instantly finish free dojo decos (e.g. obstacle course gates) (#1321) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1321 --- .../api/placeDecoInComponentController.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index cc96a6b8..08f814da 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -24,17 +24,25 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = } component.Decos ??= []; - component.Decos.push({ - _id: new Types.ObjectId(), - Type: request.Type, - Pos: request.Pos, - Rot: request.Rot, - Name: request.Name - }); + const deco = + component.Decos[ + component.Decos.push({ + _id: new Types.ObjectId(), + Type: request.Type, + Pos: request.Pos, + Rot: request.Rot, + Name: request.Name + }) - 1 + ]; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type); - if (meta && meta.capacityCost) { - component.DecoCapacity -= meta.capacityCost; + if (meta) { + if (meta.capacityCost) { + component.DecoCapacity -= meta.capacityCost; + } + if (meta.price == 0 && meta.ingredients.length == 0) { + deco.CompletionTime = new Date(); + } } await guild.save(); From eccea4ae5488e7cc46d85b886575cb5801abce09 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 06:38:30 -0700 Subject: [PATCH 256/776] feat: nightwave challenge rotation (#1317) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1317 --- .../dynamic/worldStateController.ts | 121 +++++++++++++++++- .../worldState/worldState.json | 73 ----------- 2 files changed, 114 insertions(+), 80 deletions(-) diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 7632bbe5..1de8422d 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -9,9 +9,16 @@ import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; -import { ExportRegions } from "warframe-public-export-plus"; +import { ExportNightwave, ExportRegions } from "warframe-public-export-plus"; + +const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 export const worldStateController: RequestHandler = (req, res) => { + const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const worldState: IWorldState = { BuildLabel: typeof req.query.buildLabel == "string" @@ -22,6 +29,42 @@ export const worldStateController: RequestHandler = (req, res) => { GlobalUpgrades: [], LiteSorties: [], EndlessXpChoices: [], + SeasonInfo: { + Activation: { $date: { $numberLong: "1715796000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + AffiliationTag: "RadioLegionIntermission12Syndicate", + Season: 14, + Phase: 0, + Params: "", + ActiveChallenges: [ + getSeasonDailyChallenge(day - 2), + getSeasonDailyChallenge(day - 1), + getSeasonDailyChallenge(day - 0), + getSeasonWeeklyChallenge(week, 0), + getSeasonWeeklyChallenge(week, 1), + getSeasonWeeklyHardChallenge(week, 2), + getSeasonWeeklyHardChallenge(week, 3), + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: + "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }, + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }, + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + } + ] + }, ...staticWorldState }; @@ -42,12 +85,6 @@ export const worldStateController: RequestHandler = (req, res) => { }); } - const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 - const day = Math.trunc((Date.now() - EPOCH) / 86400000); - const week = Math.trunc(day / 7); - const weekStart = EPOCH + week * 604800000; - const weekEnd = weekStart + 604800000; - // Elite Sanctuary Onslaught cycling every week worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful @@ -264,6 +301,15 @@ interface IWorldState { LiteSorties: ILiteSortie[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; + SeasonInfo: { + Activation: IMongoDate; + Expiry: IMongoDate; + AffiliationTag: string; + Season: number; + Phase: number; + Params: string; + ActiveChallenges: ISeasonChallenge[]; + }; KnownCalendarSeasons: ICalendarSeason[]; Tmp?: string; } @@ -333,6 +379,14 @@ interface IEndlessXpChoice { Choices: string[]; } +interface ISeasonChallenge { + _id: IOid; + Daily?: boolean; + Activation: IMongoDate; + Expiry: IMongoDate; + Challenge: string; +} + interface ICalendarSeason { Activation: IMongoDate; Expiry: IMongoDate; @@ -342,3 +396,56 @@ interface ICalendarSeason { }[]; YearIteration: number; } + +const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") +); + +const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { + const dayStart = EPOCH + day * 86400000; + const dayEnd = EPOCH + (day + 3) * 86400000; + const rng = new CRng(day); + return { + _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, + Daily: true, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Challenge: rng.randomElement(dailyChallenges) + }; +}; + +const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter( + x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && + !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") +); + +const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new CRng(challengeId); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(weeklyChallenges) + }; +}; + +const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") +); + +const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new CRng(challengeId); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(weeklyHardChallenges) + }; +}; diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 9a93bcb8..74463e4d 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -2976,79 +2976,6 @@ "TwitchPromos": [], "ExperimentRecommended": [], "ForceLogoutVersion": 0, - "SeasonInfo": { - "Activation": { "$date": { "$numberLong": "1715796000000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "AffiliationTag": "RadioLegionIntermission12Syndicate", - "Season": 14, - "Phase": 0, - "Params": "", - "ActiveChallenges": [ - { - "_id": { "$oid": "001300010000000000000008" }, - "Daily": true, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Daily/SeasonDailyFeedMeMore" - }, - { - "_id": { "$oid": "001300010000000000000009" }, - "Daily": true, - "Activation": { "$date": { "$numberLong": "1715644800000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Daily/SeasonDailyTwoForOne" - }, - { - "_id": { "$oid": "001300010000000000000010" }, - "Daily": true, - "Activation": { "$date": { "$numberLong": "1715731200000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Daily/SeasonDailyKillEnemiesWithFinishers" - }, - { - "_id": { "$oid": "001300010000000000000001" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" - }, - { - "_id": { "$oid": "001300010000000000000002" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" - }, - { - "_id": { "$oid": "001300010000000000000003" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" - }, - { - "_id": { "$oid": "001300010000000000000004" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyOpenLockers" - }, - { - "_id": { "$oid": "001300010000000000000005" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyBloodthirsty" - }, - { - "_id": { "$oid": "001300010000000000000006" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/WeeklyHard/SeasonWeeklyHardEliteSanctuaryOnslaught" - }, - { - "_id": { "$oid": "001300010000000000000007" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/WeeklyHard/SeasonWeeklyHardCompleteSortie" - } - ] - }, "KnownCalendarSeasons": [ { "Activation": { "$date": { "$numberLong": "1733961600000" } }, From 58508a026007be5b9be70cfe5dca44d06fe968ed Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 06:38:37 -0700 Subject: [PATCH 257/776] feat: nightwave challenge completion (#1319) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1319 --- .../api/updateChallengeProgressController.ts | 53 ++++++++++++++++--- src/services/inventoryService.ts | 13 ++--- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index a69e995b..3e056538 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -3,21 +3,60 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportNightwave } from "warframe-public-export-plus"; +import { logger } from "@/src/utils/logger"; +import { IAffiliationMods } from "@/src/types/purchaseTypes"; export const updateChallengeProgressController: RequestHandler = async (req, res) => { const challenges = getJSONfromString(String(req.body)); const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory"); - addChallenges(inventory, challenges.ChallengeProgress); - addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); + const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations"); + if (challenges.ChallengeProgress) { + addChallenges(inventory, challenges.ChallengeProgress); + } + if (challenges.SeasonChallengeHistory) { + addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); + } + const affiliationMods: IAffiliationMods[] = []; + if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) { + for (const challenge of challenges.SeasonChallengeCompletions) { + // Ignore challenges that weren't completed just now + if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) { + continue; + } + + const meta = ExportNightwave.challenges[challenge.challenge]; + logger.debug("Completed challenge", meta); + + let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag); + if (!affiliation) { + affiliation = + inventory.Affiliations[ + inventory.Affiliations.push({ + Tag: ExportNightwave.affiliationTag, + Standing: 0 + }) - 1 + ]; + } + affiliation.Standing += meta.standing; + + if (affiliationMods.length == 0) { + affiliationMods.push({ Tag: ExportNightwave.affiliationTag }); + } + affiliationMods[0].Standing ??= 0; + affiliationMods[0].Standing += meta.standing; + } + } await inventory.save(); - res.status(200).end(); + res.json({ + AffiliationMods: affiliationMods + }); }; interface IUpdateChallengeProgressRequest { - ChallengeProgress: IChallengeProgress[]; - SeasonChallengeHistory: ISeasonChallenge[]; - SeasonChallengeCompletions: ISeasonChallenge[]; + ChallengeProgress?: IChallengeProgress[]; + SeasonChallengeHistory?: ISeasonChallenge[]; + SeasonChallengeCompletions?: ISeasonChallenge[]; } diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index dde254f1..8397edfa 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1207,11 +1207,11 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus export const addSeasonalChallengeHistory = ( inventory: TInventoryDatabaseDocument, - itemsArray: ISeasonChallenge[] | undefined + itemsArray: ISeasonChallenge[] ): void => { const category = inventory.SeasonChallengeHistory; - itemsArray?.forEach(({ challenge, id }) => { + itemsArray.forEach(({ challenge, id }) => { const itemIndex = category.findIndex(i => i.challenge === challenge); if (itemIndex !== -1) { @@ -1222,17 +1222,14 @@ export const addSeasonalChallengeHistory = ( }); }; -export const addChallenges = ( - inventory: TInventoryDatabaseDocument, - itemsArray: IChallengeProgress[] | undefined -): void => { +export const addChallenges = (inventory: TInventoryDatabaseDocument, itemsArray: IChallengeProgress[]): void => { const category = inventory.ChallengeProgress; - itemsArray?.forEach(({ Name, Progress }) => { + itemsArray.forEach(({ Name, Progress }) => { const itemIndex = category.findIndex(i => i.Name === Name); if (itemIndex !== -1) { - category[itemIndex].Progress += Progress; + category[itemIndex].Progress = Progress; } else { category.push({ Name, Progress }); } From bfcd928fdecca366cbafdfa5aa870c2280e4d9de Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 06:38:44 -0700 Subject: [PATCH 258/776] feat: nightwave rank up rewards (#1320) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1320 --- .../api/clearNewEpisodeRewardController.ts | 6 +++++ .../api/syndicateSacrificeController.ts | 26 ++++++++++++++++--- src/routes/api.ts | 2 ++ src/services/itemDataService.ts | 4 +++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/controllers/api/clearNewEpisodeRewardController.ts diff --git a/src/controllers/api/clearNewEpisodeRewardController.ts b/src/controllers/api/clearNewEpisodeRewardController.ts new file mode 100644 index 00000000..1dd3010a --- /dev/null +++ b/src/controllers/api/clearNewEpisodeRewardController.ts @@ -0,0 +1,6 @@ +import { RequestHandler } from "express"; + +// example req.body: {"NewEpisodeReward":true,"crossPlaySetting":"ENABLED"} +export const clearNewEpisodeRewardController: RequestHandler = (_req, res) => { + res.status(200).end(); +}; diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index 77d2424a..b20df3bf 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -1,10 +1,17 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; +import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { + addItem, + addMiscItems, + combineInventoryChanges, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; export const syndicateSacrificeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -22,7 +29,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp InventoryChanges: {}, Level: data.SacrificeLevel, LevelIncrease: level <= 0 ? 1 : level, - NewEpisodeReward: syndicate.Tag == "RadioLegionIntermission9Syndicate" + NewEpisodeReward: false }; const manifest = ExportSyndicates[data.AffiliationTag]; @@ -64,6 +71,19 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp ); } + if (data.AffiliationTag == ExportNightwave.affiliationTag) { + const index = syndicate.Title - 1; + if (index < ExportNightwave.rewards.length) { + res.NewEpisodeReward = true; + const reward = ExportNightwave.rewards[index]; + let rewardType = reward.uniqueName; + if (isStoreItem(rewardType)) { + rewardType = fromStoreItem(rewardType); + } + combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1)); + } + } + await inventory.save(); response.json(res); diff --git a/src/routes/api.ts b/src/routes/api.ts index b6fd4316..e2f1dbea 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -15,6 +15,7 @@ import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDai import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; +import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController"; import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; @@ -175,6 +176,7 @@ apiRouter.post("/artifactTransmutation.php", artifactTransmutationController); apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); +apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index b1a1a297..9be6c180 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -250,6 +250,10 @@ export const convertInboxMessage = (message: IInboxMessage): IMessage => { } satisfies IMessage; }; +export const isStoreItem = (type: string): boolean => { + return type.startsWith("/Lotus/StoreItems/"); +}; + export const toStoreItem = (type: string): string => { return "/Lotus/StoreItems/" + type.substring("/Lotus/".length); }; From 06ce4ac69551f9f0f6731d0957b9badd085cd7f2 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 15:11:26 -0700 Subject: [PATCH 259/776] chore: more faithful handling of daily tribute (#1324) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1324 --- src/controllers/api/loginRewardsController.ts | 29 +++++++++++++------ .../api/loginRewardsSelectionController.ts | 2 +- src/models/loginModel.ts | 2 +- src/services/loginRewardService.ts | 18 ++++++++++-- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/loginRewardsController.ts b/src/controllers/api/loginRewardsController.ts index f6430e28..16d77261 100644 --- a/src/controllers/api/loginRewardsController.ts +++ b/src/controllers/api/loginRewardsController.ts @@ -1,31 +1,42 @@ import { RequestHandler } from "express"; import { getAccountForRequest } from "@/src/services/loginService"; -import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse } from "@/src/services/loginRewardService"; +import { + claimLoginReward, + getRandomLoginRewards, + ILoginRewardsReponse, + isLoginRewardAChoice +} from "@/src/services/loginRewardService"; import { getInventory } from "@/src/services/inventoryService"; export const loginRewardsController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); const today = Math.trunc(Date.now() / 86400000) * 86400; + const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; + const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50; if (today == account.LastLoginRewardDate) { - res.end(); + res.json({ + DailyTributeInfo: { + IsMilestoneDay: isMilestoneDay, + IsChooseRewardSet: isLoginRewardAChoice(account), + LoginDays: account.LoginDays, + NextMilestoneReward: "", + NextMilestoneDay: nextMilestoneDay + } + } satisfies ILoginRewardsReponse); return; } - account.LoginDays += 1; - account.LastLoginRewardDate = today; - await account.save(); const inventory = await getInventory(account._id.toString()); const randomRewards = getRandomLoginRewards(account, inventory); - const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; const response: ILoginRewardsReponse = { DailyTributeInfo: { Rewards: randomRewards, IsMilestoneDay: isMilestoneDay, IsChooseRewardSet: randomRewards.length != 1, LoginDays: account.LoginDays, - //NextMilestoneReward: "", - NextMilestoneDay: account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50, + NextMilestoneReward: "", + NextMilestoneDay: nextMilestoneDay, HasChosenReward: false }, LastLoginRewardDate: today @@ -33,7 +44,7 @@ export const loginRewardsController: RequestHandler = async (req, res) => { if (!isMilestoneDay && randomRewards.length == 1) { response.DailyTributeInfo.HasChosenReward = true; response.DailyTributeInfo.ChosenReward = randomRewards[0]; - response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); + response.DailyTributeInfo.NewInventory = await claimLoginReward(account, inventory, randomRewards[0]); await inventory.save(); } res.json(response); diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts index b1ebc9a6..8bd14dd7 100644 --- a/src/controllers/api/loginRewardsSelectionController.ts +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -28,7 +28,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res) } else { const randomRewards = getRandomLoginRewards(account, inventory); chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; - inventoryChanges = await claimLoginReward(inventory, chosenReward); + inventoryChanges = await claimLoginReward(account, inventory, chosenReward); } await inventory.save(); res.json({ diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 2218a27d..7b12c07a 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -23,7 +23,7 @@ const databaseAccountSchema = new Schema( Dropped: Boolean, LatestEventMessageDate: { type: Date, default: 0 }, LastLoginRewardDate: { type: Number, default: 0 }, - LoginDays: { type: Number, default: 0 } + LoginDays: { type: Number, default: 1 } }, opts ); diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index 4d5c562a..5e7c442e 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -14,7 +14,7 @@ export interface ILoginRewardsReponse { IsMilestoneDay?: boolean; IsChooseRewardSet?: boolean; LoginDays?: number; // when calling multiple times per day, this is already incremented to represent "tomorrow" - //NextMilestoneReward?: ""; + NextMilestoneReward?: ""; NextMilestoneDay?: number; // seems to not be used if IsMilestoneDay HasChosenReward?: boolean; NewInventory?: IInventoryChanges; @@ -46,6 +46,13 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu return amount + Math.min(day, 3000) / divisor; }; +// Always produces the same result for the same account _id & LoginDays pair. +export const isLoginRewardAChoice = (account: TAccountDocument): boolean => { + const accountSeed = parseInt(account._id.toString().substring(16), 16); + const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); + return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed. +}; + // Always produces the same result for the same account _id & LoginDays pair. export const getRandomLoginRewards = ( account: TAccountDocument, @@ -53,9 +60,9 @@ export const getRandomLoginRewards = ( ): ILoginReward[] => { const accountSeed = parseInt(account._id.toString().substring(16), 16); const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); + const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed. const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)]; - // Using 25% an approximate chance for pick-a-doors. More conclusive data analysis is needed. - if (rng.random() < 0.25) { + if (pick_a_door) { do { const reward = getRandomLoginReward(rng, account.LoginDays, inventory); if (!rewards.find(x => x.StoreItemType == reward.StoreItemType)) { @@ -114,9 +121,14 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab }; export const claimLoginReward = async ( + account: TAccountDocument, inventory: TInventoryDatabaseDocument, reward: ILoginReward ): Promise => { + account.LoginDays += 1; + account.LastLoginRewardDate = Math.trunc(Date.now() / 86400000) * 86400; + await account.save(); + switch (reward.RewardType) { case "RT_RESOURCE": case "RT_STORE_ITEM": From 5597bfe8767b0b2cbc6c0205b875542d8afc818e Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 15:12:09 -0700 Subject: [PATCH 260/776] feat: custom obstacle course leaderboard (#1326) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1326 --- ...stomObstacleCourseLeaderboardController.ts | 48 +++++++++++++++++++ src/models/guildModel.ts | 15 +++++- src/routes/api.ts | 2 + src/types/guildTypes.ts | 7 +++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/customObstacleCourseLeaderboardController.ts diff --git a/src/controllers/api/customObstacleCourseLeaderboardController.ts b/src/controllers/api/customObstacleCourseLeaderboardController.ts new file mode 100644 index 00000000..9b768def --- /dev/null +++ b/src/controllers/api/customObstacleCourseLeaderboardController.ts @@ -0,0 +1,48 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + const guild = (await Guild.findById(data.g, "DojoComponents"))!; + const component = guild.DojoComponents.id(data.c)!; + if (req.query.act == "f") { + res.json({ + results: component.Leaderboard ?? [] + }); + } else if (req.query.act == "p") { + const account = await getAccountForRequest(req); + component.Leaderboard ??= []; + const entry = component.Leaderboard.find(x => x.n == account.DisplayName); + if (entry) { + entry.s = data.s!; + } else { + component.Leaderboard.push({ + s: data.s!, + n: account.DisplayName, + r: 0 + }); + } + component.Leaderboard.sort((a, b) => a.s - b.s); // In this case, the score is the time in milliseconds, so smaller is better. + if (component.Leaderboard.length > 10) { + component.Leaderboard.shift(); + } + let r = 0; + for (const entry of component.Leaderboard) { + entry.r = ++r; + } + await guild.save(); + res.status(200).end(); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unknown customObstacleCourseLeaderboard act: ${String(req.query.act)}`); + } +}; + +interface ICustomObstacleCourseLeaderboardRequest { + g: string; + c: string; + s?: number; // act=p +} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index d0f7c501..bfd3e4d3 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -9,7 +9,8 @@ import { IGuildRank, IGuildLogRoomChange, IGuildLogEntryRoster, - IGuildLogEntryContributable + IGuildLogEntryContributable, + IDojoLeaderboardEntry } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -25,6 +26,15 @@ const dojoDecoSchema = new Schema({ RushPlatinum: Number }); +const dojoLeaderboardEntrySchema = new Schema( + { + s: Number, + r: Number, + n: String + }, + { _id: false } +); + const dojoComponentSchema = new Schema({ pf: { type: String, required: true }, ppf: String, @@ -40,7 +50,8 @@ const dojoComponentSchema = new Schema({ RushPlatinum: Number, DestructionTime: Date, Decos: [dojoDecoSchema], - DecoCapacity: Number + DecoCapacity: Number, + Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined } }); const techProjectSchema = new Schema( diff --git a/src/routes/api.ts b/src/routes/api.ts index e2f1dbea..6489cfc8 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -24,6 +24,7 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; +import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; @@ -183,6 +184,7 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); +apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); apiRouter.post("/drones.php", dronesController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 7d10b57d..da444a2b 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -152,6 +152,7 @@ export interface IDojoComponentDatabase CompletionLogPending?: boolean; DestructionTime?: Date; Decos?: IDojoDecoDatabase[]; + Leaderboard?: IDojoLeaderboardEntry[]; } export interface IDojoDecoClient { @@ -212,3 +213,9 @@ export interface IGuildLogEntryNumber { entryType: number; details: number; } + +export interface IDojoLeaderboardEntry { + s: number; // score + r: number; // rank + n: string; // displayName +} From 82216740982f84392917ea3f08cd2118aa6e043b Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 15:12:20 -0700 Subject: [PATCH 261/776] chore(webui): handle index.html being opened as a file (#1329) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1329 --- static/webui/index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/static/webui/index.html b/static/webui/index.html index 56e95ac8..f88820e9 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -619,5 +619,12 @@ + From 0fc1326255a7d4e3294db0e6dd3da236ab249e69 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 25 Mar 2025 15:12:31 -0700 Subject: [PATCH 262/776] fix(webui): refresh inventory after changing server cheats (#1331) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1331 --- static/webui/script.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/webui/script.js b/static/webui/script.js index d786b2b4..530e8091 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -967,6 +967,9 @@ function doChangeSettings() { url: "/custom/config?" + window.authz, contentType: "text/plain", data: JSON.stringify(json, null, 2) + }).then(() => { + // A few cheats affect the inventory response which in turn may change what values we need to show + updateInventory(); }); }); } From aea1787908d8ac770ae60063934d68be8c415574 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 00:28:35 +0100 Subject: [PATCH 263/776] chore: handle nameFromEmail being empty --- src/controllers/api/loginController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 9f70d0ad..19b3c380 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -24,7 +24,7 @@ export const loginController: RequestHandler = async (request, response) => { if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { try { const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); - let name = nameFromEmail; + let name = nameFromEmail || "SpaceNinja"; if (await isNameTaken(name)) { let suffix = 0; do { From 0f7866a575ad50ff94e1b76bb2cf2df6bb53fb6b Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 05:09:48 -0700 Subject: [PATCH 264/776] fix: handle weapon meta having an empty defaultUpgrades array (#1333) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1333 --- src/services/inventoryService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 8397edfa..39361841 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -378,8 +378,8 @@ export const addItem = async ( defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; } if ( - weapon.defaultUpgrades && - weapon.defaultUpgrades[0].ItemType == "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod" + weapon.defaultUpgrades?.[0]?.ItemType == + "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod" ) { defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod"; defaultOverwrites.UpgradeFingerprint = JSON.stringify({ From 049f709713e7c7900c08c2f7479ea06b61076962 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:21:22 -0700 Subject: [PATCH 265/776] feat(leaderboard): missions & guilds leaderboard (#1338) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1338 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../stats/leaderboardController.ts | 10 +++++- src/models/leaderboardModel.ts | 3 +- src/services/leaderboardService.ts | 21 ++++++++--- src/services/statsService.ts | 35 ++++++++++++++++--- src/types/leaderboardTypes.ts | 1 + 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/controllers/stats/leaderboardController.ts b/src/controllers/stats/leaderboardController.ts index b76e5e16..8576c92d 100644 --- a/src/controllers/stats/leaderboardController.ts +++ b/src/controllers/stats/leaderboardController.ts @@ -6,7 +6,14 @@ export const leaderboardController: RequestHandler = async (req, res) => { logger.debug(`data provided to ${req.path}: ${String(req.body)}`); const payload = JSON.parse(String(req.body)) as ILeaderboardRequest; res.json({ - results: await getLeaderboard(payload.field, payload.before, payload.after, payload.guildId, payload.pivotId) + results: await getLeaderboard( + payload.field, + payload.before, + payload.after, + payload.guildId, + payload.pivotId, + payload.guildTier + ) }); }; @@ -16,4 +23,5 @@ interface ILeaderboardRequest { after: number; guildId?: string; pivotId?: string; + guildTier?: number; } diff --git a/src/models/leaderboardModel.ts b/src/models/leaderboardModel.ts index 5de2f608..0faebddc 100644 --- a/src/models/leaderboardModel.ts +++ b/src/models/leaderboardModel.ts @@ -8,7 +8,8 @@ const leaderboardEntrySchema = new Schema( displayName: { type: String, required: true }, score: { type: Number, required: true }, guildId: Schema.Types.ObjectId, - expiry: { type: Date, required: true } + expiry: { type: Date, required: true }, + guildTier: Number }, { id: false } ); diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts index afbe1623..097125bb 100644 --- a/src/services/leaderboardService.ts +++ b/src/services/leaderboardService.ts @@ -1,14 +1,15 @@ +import { Guild } from "../models/guildModel"; import { Leaderboard, TLeaderboardEntryDocument } from "../models/leaderboardModel"; import { ILeaderboardEntryClient } from "../types/leaderboardTypes"; export const submitLeaderboardScore = async ( + schedule: "weekly" | "daily", leaderboard: string, ownerId: string, displayName: string, score: number, guildId?: string ): Promise => { - const schedule = leaderboard.split(".")[0] as "daily" | "weekly"; let expiry: Date; if (schedule == "daily") { expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); @@ -21,10 +22,18 @@ export const submitLeaderboardScore = async ( expiry = new Date(weekEnd); } await Leaderboard.findOneAndUpdate( - { leaderboard, ownerId }, + { leaderboard: `${schedule}.accounts.${leaderboard}`, ownerId }, { $max: { score }, $set: { displayName, guildId, expiry } }, { upsert: true } ); + if (guildId) { + const guild = (await Guild.findById(guildId, "Name Tier"))!; + await Leaderboard.findOneAndUpdate( + { leaderboard: `${schedule}.guilds.${leaderboard}`, ownerId: guildId }, + { $max: { score }, $set: { displayName: guild.Name, guildTier: guild.Tier, expiry } }, + { upsert: true } + ); + } }; export const getLeaderboard = async ( @@ -32,12 +41,16 @@ export const getLeaderboard = async ( before: number, after: number, guildId?: string, - pivotId?: string + pivotId?: string, + guildTier?: number ): Promise => { - const filter: { leaderboard: string; guildId?: string } = { leaderboard }; + const filter: { leaderboard: string; guildId?: string; guildTier?: number } = { leaderboard }; if (guildId) { filter.guildId = guildId; } + if (guildTier) { + filter.guildTier = guildTier; + } let entries: TLeaderboardEntryDocument[]; let r: number; diff --git a/src/services/statsService.ts b/src/services/statsService.ts index a6ca826c..601aae7b 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -286,6 +286,15 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) } else { playerStats.Missions.push({ type: type, highScore }); } + await submitLeaderboardScore( + "weekly", + type, + accountOwnerId, + payload.displayName, + highScore, + payload.guildId + ); + break; } break; @@ -304,27 +313,42 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) } await submitLeaderboardScore( - "daily.accounts." + race, + "daily", + race, accountOwnerId, payload.displayName, - highScore + highScore, + payload.guildId ); } break; case "ZephyrScore": - case "SentinelGameScore": case "CaliberChicksScore": playerStats[category] ??= 0; if (data > playerStats[category]) playerStats[category] = data as number; break; + case "SentinelGameScore": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "weekly", + category, + accountOwnerId, + payload.displayName, + data as number, + payload.guildId + ); + break; + case "DojoObstacleScore": playerStats[category] ??= 0; if (data > playerStats[category]) playerStats[category] = data as number; await submitLeaderboardScore( - "weekly.accounts." + category, + "weekly", + category, accountOwnerId, payload.displayName, data as number, @@ -350,7 +374,8 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) } if (data > playerStats[category]) playerStats[category] = data as number; await submitLeaderboardScore( - "weekly.accounts." + category, + "weekly", + category, accountOwnerId, payload.displayName, data as number diff --git a/src/types/leaderboardTypes.ts b/src/types/leaderboardTypes.ts index 5173a3a3..ed14c94d 100644 --- a/src/types/leaderboardTypes.ts +++ b/src/types/leaderboardTypes.ts @@ -7,6 +7,7 @@ export interface ILeaderboardEntryDatabase { score: number; guildId?: Types.ObjectId; expiry: Date; + guildTier?: number; } export interface ILeaderboardEntryClient { From 401f1ed229375a184a235fb70e2b775270a471d3 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 14:21:58 -0700 Subject: [PATCH 266/776] feat: hubBlessing.php (#1335) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1335 --- src/controllers/api/hubBlessingController.ts | 45 ++++++++++++++++++++ src/routes/api.ts | 2 + 2 files changed, 47 insertions(+) create mode 100644 src/controllers/api/hubBlessingController.ts diff --git a/src/controllers/api/hubBlessingController.ts b/src/controllers/api/hubBlessingController.ts new file mode 100644 index 00000000..49b992e8 --- /dev/null +++ b/src/controllers/api/hubBlessingController.ts @@ -0,0 +1,45 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addBooster, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomInt } from "@/src/services/rngService"; +import { RequestHandler } from "express"; +import { ExportBoosters } from "warframe-public-export-plus"; + +export const hubBlessingController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const boosterType = ExportBoosters[data.booster].typeName; + if (req.query.mode == "send") { + const inventory = await getInventory(accountId, "BlessingCooldown Boosters"); + inventory.BlessingCooldown = new Date(Date.now() + 86400000); + addBooster(boosterType, 3 * 3600, inventory); + await inventory.save(); + + let token = ""; + for (let i = 0; i != 32; ++i) { + token += getRandomInt(0, 15).toString(16); + } + + res.json({ + BlessingCooldown: inventory.BlessingCooldown, + SendTime: Math.trunc(Date.now() / 1000).toString(), + Token: token + }); + } else { + const inventory = await getInventory(accountId, "Boosters"); + addBooster(boosterType, 3 * 3600, inventory); + await inventory.save(); + + res.json({ + BoosterType: data.booster, + Sender: data.senderId + }); + } +}; + +interface IHubBlessingRequest { + booster: string; + senderId?: string; // mode=request + sendTime?: string; // mode=request + token?: string; // mode=request +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 6489cfc8..686a5a61 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -57,6 +57,7 @@ import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; import { guildTechController } from "@/src/controllers/api/guildTechController"; import { hostSessionController } from "@/src/controllers/api/hostSessionController"; +import { hubBlessingController } from "@/src/controllers/api/hubBlessingController"; import { hubController } from "@/src/controllers/api/hubController"; import { hubInstancesController } from "@/src/controllers/api/hubInstancesController"; import { inboxController } from "@/src/controllers/api/inboxController"; @@ -207,6 +208,7 @@ apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); apiRouter.post("/giveStartingGear.php", giveStartingGearController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); +apiRouter.post("/hubBlessing.php", hubBlessingController); apiRouter.post("/infestedFoundry.php", infestedFoundryController); apiRouter.post("/inventorySlots.php", inventorySlotsController); apiRouter.post("/joinSession.php", joinSessionController); From 83b267bcf514aaff042c87ac28f85e9e19454e48 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 14:22:09 -0700 Subject: [PATCH 267/776] fix: restrict transmutation polarity when a transmute core is being used (#1336) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1336 --- src/controllers/api/artifactTransmutationController.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts index 3fd52535..78da93a1 100644 --- a/src/controllers/api/artifactTransmutationController.ts +++ b/src/controllers/api/artifactTransmutationController.ts @@ -53,6 +53,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res) RARE: 0, LEGENDARY: 0 }; + let forcedPolarity: string | undefined; payload.Consumed.forEach(upgrade => { const meta = ExportUpgrades[upgrade.ItemType]; counts[meta.rarity] += upgrade.ItemCount; @@ -62,6 +63,13 @@ export const artifactTransmutationController: RequestHandler = async (req, res) ItemCount: upgrade.ItemCount * -1 } ]); + if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") { + forcedPolarity = "AP_ATTACK"; + } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") { + forcedPolarity = "AP_DEFENSE"; + } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/TacticTransmuteCore") { + forcedPolarity = "AP_TACTIC"; + } }); // Based on the table on https://wiki.warframe.com/w/Transmutation @@ -74,7 +82,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res) const options: { uniqueName: string; rarity: TRarity }[] = []; Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => { - if (upgrade.canBeTransmutation) { + if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) { options.push({ uniqueName, rarity: upgrade.rarity }); } }); From 926b87dda07930210fe9ec4473e3e0d86b3ea6ed Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 22:46:30 +0100 Subject: [PATCH 268/776] chore: cleanup leaderboards stuff --- src/controllers/stats/leaderboardController.ts | 6 ++---- src/services/leaderboardService.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/controllers/stats/leaderboardController.ts b/src/controllers/stats/leaderboardController.ts index 8576c92d..f5550f2b 100644 --- a/src/controllers/stats/leaderboardController.ts +++ b/src/controllers/stats/leaderboardController.ts @@ -1,17 +1,15 @@ import { getLeaderboard } from "@/src/services/leaderboardService"; -import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; export const leaderboardController: RequestHandler = async (req, res) => { - logger.debug(`data provided to ${req.path}: ${String(req.body)}`); const payload = JSON.parse(String(req.body)) as ILeaderboardRequest; res.json({ results: await getLeaderboard( payload.field, payload.before, payload.after, - payload.guildId, payload.pivotId, + payload.guildId, payload.guildTier ) }); @@ -21,7 +19,7 @@ interface ILeaderboardRequest { field: string; before: number; after: number; - guildId?: string; pivotId?: string; + guildId?: string; guildTier?: number; } diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts index 097125bb..c4084d6e 100644 --- a/src/services/leaderboardService.ts +++ b/src/services/leaderboardService.ts @@ -40,8 +40,8 @@ export const getLeaderboard = async ( leaderboard: string, before: number, after: number, - guildId?: string, pivotId?: string, + guildId?: string, guildTier?: number ): Promise => { const filter: { leaderboard: string; guildId?: string; guildTier?: number } = { leaderboard }; From 7492ddaad7c6a715c819b478a5f8c352a83d3b32 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 16:08:33 -0700 Subject: [PATCH 269/776] feat: handle CapturedAnimals in missionInventoryUpdate (#1337) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1337 --- src/services/missionInventoryUpdateService.ts | 34 ++ src/types/requestTypes.ts | 7 + .../fixed_responses/conservationAnimals.json | 491 ++++++++++++++++++ 3 files changed, 532 insertions(+) create mode 100644 static/fixed_responses/conservationAnimals.json diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 86561ed2..d8b1cc32 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -43,6 +43,7 @@ import { createMessage } from "./inboxService"; import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json"; import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; +import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; import { getInfNodes } from "@/src/helpers/nemesisHelpers"; const getRotations = (rotationCount: number): number[] => { @@ -325,6 +326,39 @@ export const addMissionInventoryUpdates = async ( inventory.DeathMarks = value; break; } + case "CapturedAnimals": { + for (const capturedAnimal of value) { + const meta = conservationAnimals[capturedAnimal.AnimalType as keyof typeof conservationAnimals]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (meta) { + if (capturedAnimal.NumTags) { + addMiscItems(inventory, [ + { + ItemType: meta.tag, + ItemCount: capturedAnimal.NumTags + } + ]); + } + if (capturedAnimal.NumExtraRewards) { + if ("extraReward" in meta) { + addMiscItems(inventory, [ + { + ItemType: meta.extraReward, + ItemCount: capturedAnimal.NumExtraRewards + } + ]); + } else { + logger.warn( + `client attempted to claim unknown extra rewards for conservation of ${capturedAnimal.AnimalType}` + ); + } + } + } else { + logger.warn(`ignoring conservation of unknown AnimalType: ${capturedAnimal.AnimalType}`); + } + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 9b977d78..b61c99da 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -99,6 +99,13 @@ export type IMissionInventoryUpdateRequest = { DeathMarks?: string[]; Nemesis?: number; Boosters?: IBooster[]; + CapturedAnimals?: { + AnimalType: string; + CaptureRating: number; + NumTags: number; + NumExtraRewards: number; + Count: number; + }[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; diff --git a/static/fixed_responses/conservationAnimals.json b/static/fixed_responses/conservationAnimals.json new file mode 100644 index 00000000..81dc1c0e --- /dev/null +++ b/static/fixed_responses/conservationAnimals.json @@ -0,0 +1,491 @@ +{ + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonFemaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonMaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareFemaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareMaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonFemaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonMaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/BaseInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/CommonInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedCritterCommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedCritterCommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/RareInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedCritterRare", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedCritterRareRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/UncommonInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedCritterUncommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedCritterUncommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/GrottoInfKDriveAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedKdriveUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/HighlandInfKDriveAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedKdriveRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/SwampInfKDriveAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedKdriveCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/CommonInfestedMaggotAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMaggotCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/RareInfestedMaggotAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMaggotRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/UncommonInfestedMaggotAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMaggotUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/CommonInfestedMergooAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/RareInfestedMergooAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/UncommonInfestedMergooAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/BaseInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaCommon", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/CommonInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaCommon", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/RareInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaRare", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/UncommonInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaUncommon", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/BaseInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/CommonInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedPredatorCommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedPredatorCommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/RareInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedPredatorRare", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedPredatorRareRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/UncommonInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedPredatorUncommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedPredatorUncommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/BaseUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/CommonUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/RareUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/UncommonUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/BaseDuviriRabbitAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/TeshinRabbitAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/TeshinRabbitOnHandAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Wolf/DuviriWolfAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Wolf/DuviriWolfConservationAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonFemaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonMaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareFemaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareMaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/TutorialForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonFemaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonMaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/BaseLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonFemaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonMaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonPupLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareFemaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareMaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RarePupLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonFemaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonMaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonPupLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/BaseOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonFemaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonMaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonPupOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareFemaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareMaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RarePupOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonFemaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonMaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonPupOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonFemaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonMaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareFemaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareMaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonFemaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonMaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/BaseSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonFemaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonMaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonPupSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareFemaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareMaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RarePupSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonFemaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonMaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonPupSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/BaseSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonFemaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonMaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonPupSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareFemaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareMaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RarePupSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonFemaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonMaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonPupSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/BaseSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonFemaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonMaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonPupSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareFemaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareMaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RarePupSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonFemaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonMaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonPupSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/BaseSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonFemaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonMaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonPupSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareFemaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareMaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RarePupSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonFemaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonMaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonPupSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/BaseSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonFemaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonMaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareFemaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareMaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonFemaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonMaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/BaseVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatCubAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatFemaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatMaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatCubAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatFemaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatMaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatCubAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatFemaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatMaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + } +} From 5f9475f75029119361ebb8d12f5901be71793876 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 26 Mar 2025 16:09:05 -0700 Subject: [PATCH 270/776] fix: only give normal variant blueprints from daily tribute (#1332) you definitely shouldn't get prime or kuva variants Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1332 --- src/services/loginRewardService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index 5e7c442e..f54499e5 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -83,13 +83,13 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab masteredItems.add(entry.ItemType); } const unmasteredItems = new Set(); - for (const uniqueName of Object.keys(ExportWeapons)) { - if (!masteredItems.has(uniqueName)) { + for (const [uniqueName, data] of Object.entries(ExportWeapons)) { + if (data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName)) { unmasteredItems.add(uniqueName); } } - for (const uniqueName of Object.keys(ExportWarframes)) { - if (!masteredItems.has(uniqueName)) { + for (const [uniqueName, data] of Object.entries(ExportWarframes)) { + if (data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName)) { unmasteredItems.add(uniqueName); } } From 36c7b6f8f8fcd774fce97a03678f5bee93c47ff8 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Mar 2025 03:32:50 -0700 Subject: [PATCH 271/776] feat: handle DiscoveredMarkers in missionInventoryUpdate (#1339) Closes #679 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1339 --- src/models/inventoryModels/inventoryModel.ts | 13 +++++++++++-- src/services/missionInventoryUpdateService.ts | 11 +++++++++++ src/types/requestTypes.ts | 4 +++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a2e9b4b7..7904a12f 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -81,7 +81,8 @@ import { IVendorPurchaseHistoryEntryClient, INemesisDatabase, INemesisClient, - IInfNode + IInfNode, + IDiscoveredMarker } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -377,6 +378,14 @@ droneSchema.set("toJSON", { } }); +const discoveredMarkerSchema = new Schema( + { + tag: String, + discoveryState: [Number] + }, + { _id: false } +); + const challengeProgressSchema = new Schema( { Progress: Number, @@ -1334,7 +1343,7 @@ const inventorySchema = new Schema( ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" }, // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable - DiscoveredMarkers: [Schema.Types.Mixed], + DiscoveredMarkers: [discoveredMarkerSchema], //Open location mission like "JobId" + "StageCompletions" CompletedJobs: [Schema.Types.Mixed], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index d8b1cc32..ef66afbf 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -359,6 +359,17 @@ export const addMissionInventoryUpdates = async ( } break; } + case "DiscoveredMarkers": { + for (const clientMarker of value) { + const dbMarker = inventory.DiscoveredMarkers.find(x => x.tag == clientMarker.tag); + if (dbMarker) { + dbMarker.discoveryState = clientMarker.discoveryState; + } else { + inventory.DiscoveredMarkers.push(clientMarker); + } + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index b61c99da..25158f20 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -16,7 +16,8 @@ import { IQuestKeyDatabase, ILoreFragmentScan, IUpgradeClient, - ICollectibleEntry + ICollectibleEntry, + IDiscoveredMarker } from "./inventoryTypes/inventoryTypes"; export interface IAffiliationChange { @@ -106,6 +107,7 @@ export type IMissionInventoryUpdateRequest = { NumExtraRewards: number; Count: number; }[]; + DiscoveredMarkers?: IDiscoveredMarker[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From d9b944175ade81408e6177d4849c08cdcce7166c Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Mar 2025 03:33:08 -0700 Subject: [PATCH 272/776] feat: view clan contributions (#1340) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1340 --- .../contributeToDojoComponentController.ts | 20 +++++++++++++++---- .../api/contributeToVaultController.ts | 13 ++++++++++++ .../api/dojoComponentRushController.ts | 7 +++++++ .../api/getGuildContributionsController.ts | 18 +++++++++++++++++ src/controllers/api/guildTechController.ts | 15 +++++++++++++- src/models/guildModel.ts | 6 +++++- src/routes/api.ts | 2 ++ src/services/guildService.ts | 7 +------ src/types/guildTypes.ts | 4 ++++ 9 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/controllers/api/getGuildContributionsController.ts diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 9261627e..05859ad4 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -1,4 +1,4 @@ -import { TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getDojoClient, @@ -10,7 +10,7 @@ import { } from "@/src/services/guildService"; import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { IDojoContributable } from "@/src/types/guildTypes"; +import { IDojoContributable, IGuildMemberDatabase } from "@/src/types/guildTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; @@ -35,6 +35,10 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r return; } const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; const component = guild.DojoComponents.id(request.ComponentId)!; @@ -45,7 +49,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r throw new Error("attempt to contribute to a deco in an unfinished room?!"); } const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; - processContribution(guild, request, inventory, inventoryChanges, meta, component); + processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, component); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (component.CompletionTime) { setDojoRoomLogFunded(guild, component); @@ -55,12 +59,13 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r if (request.DecoId) { const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; - processContribution(guild, request, inventory, inventoryChanges, meta, deco); + processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, deco); } } await guild.save(); await inventory.save(); + await guildMember.save(); res.json({ ...(await getDojoClient(guild, 0, component._id)), InventoryChanges: inventoryChanges @@ -69,6 +74,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r const processContribution = ( guild: TGuildDatabaseDocument, + guildMember: IGuildMemberDatabase, request: IContributeToDojoComponentRequest, inventory: TInventoryDatabaseDocument, inventoryChanges: IInventoryChanges, @@ -80,6 +86,9 @@ const processContribution = ( component.RegularCredits += request.RegularCredits; inventoryChanges.RegularCredits = -request.RegularCredits; updateCurrency(inventory, request.RegularCredits, false); + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += request.RegularCredits; } if (request.VaultCredits) { component.RegularCredits += request.VaultCredits; @@ -133,6 +142,9 @@ const processContribution = ( ItemType: ingredientContribution.ItemType, ItemCount: ingredientContribution.ItemCount * -1 }); + + guildMember.MiscItemsContributed ??= []; + guildMember.MiscItemsContributed.push(ingredientContribution); } addMiscItems(inventory, miscItemChanges); inventoryChanges.MiscItems = miscItemChanges; diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index dd8b18fa..b1ac10d8 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,3 +1,4 @@ +import { GuildMember } from "@/src/models/guildModel"; import { getGuildForRequestEx } from "@/src/services/guildService"; import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -8,23 +9,34 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed" + ))!; const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; if (request.RegularCredits) { guild.VaultRegularCredits ??= 0; guild.VaultRegularCredits += request.RegularCredits; + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += request.RegularCredits; } if (request.MiscItems.length) { guild.VaultMiscItems ??= []; + guildMember.MiscItemsContributed ??= []; for (const item of request.MiscItems) { guild.VaultMiscItems.push(item); + guildMember.MiscItemsContributed.push(item); addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } if (request.ShipDecorations.length) { guild.VaultShipDecorations ??= []; + guildMember.ShipDecorationsContributed ??= []; for (const item of request.ShipDecorations) { guild.VaultShipDecorations.push(item); + guildMember.ShipDecorationsContributed.push(item); addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } @@ -38,6 +50,7 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { await guild.save(); await inventory.save(); + await guildMember.save(); res.end(); }; diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 633b38e1..5f334356 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -1,3 +1,4 @@ +import { GuildMember } from "@/src/models/guildModel"; import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -48,6 +49,12 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { await guild.save(); await inventory.save(); + + const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!; + guildMember.PremiumCreditsContributed ??= 0; + guildMember.PremiumCreditsContributed += request.Amount; + await guildMember.save(); + res.json({ ...(await getDojoClient(guild, 0, component._id)), InventoryChanges: inventoryChanges diff --git a/src/controllers/api/getGuildContributionsController.ts b/src/controllers/api/getGuildContributionsController.ts new file mode 100644 index 00000000..72d61cbe --- /dev/null +++ b/src/controllers/api/getGuildContributionsController.ts @@ -0,0 +1,18 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const getGuildContributionsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const guildId = (await getInventory(accountId, "GuildId")).GuildId; + const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!; + res.json({ + _id: { $oid: req.query.buddyId }, + RegularCreditsContributed: guildMember.RegularCreditsContributed, + PremiumCreditsContributed: guildMember.PremiumCreditsContributed, + MiscItemsContributed: guildMember.MiscItemsContributed, + ConsumablesContributed: [], // ??? + ShipDecorationsContributed: guildMember.ShipDecorationsContributed + }); +}; diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 2faf34c7..58c505b0 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -21,7 +21,7 @@ import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; -import { TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; @@ -90,6 +90,12 @@ export const guildTechController: RequestHandler = async (req, res) => { res.status(400).send("-1").end(); return; } + + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + const contributions = data; const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; @@ -106,6 +112,9 @@ export const guildTechController: RequestHandler = async (req, res) => { } techProject.ReqCredits -= contributions.RegularCredits; + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += contributions.RegularCredits; + if (contributions.VaultMiscItems.length) { for (const miscItem of contributions.VaultMiscItems) { const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); @@ -133,6 +142,9 @@ export const guildTechController: RequestHandler = async (req, res) => { ItemType: miscItem.ItemType, ItemCount: miscItem.ItemCount * -1 }); + + guildMember.MiscItemsContributed ??= []; + guildMember.MiscItemsContributed.push(miscItem); } } addMiscItems(inventory, miscItemChanges); @@ -151,6 +163,7 @@ export const guildTechController: RequestHandler = async (req, res) => { await guild.save(); await inventory.save(); + await guildMember.save(); res.json({ InventoryChanges: inventoryChanges, Vault: getGuildVault(guild) diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index bfd3e4d3..5ba44fc1 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -215,7 +215,11 @@ const guildMemberSchema = new Schema({ accountId: Types.ObjectId, guildId: Types.ObjectId, status: { type: Number, required: true }, - rank: { type: Number, default: 7 } + rank: { type: Number, default: 7 }, + RegularCreditsContributed: Number, + PremiumCreditsContributed: Number, + MiscItemsContributed: { type: [typeCountSchema], default: undefined }, + ShipDecorationsContributed: { type: [typeCountSchema], default: undefined } }); guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true }); diff --git a/src/routes/api.ts b/src/routes/api.ts index 686a5a61..30d93d0e 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -42,6 +42,7 @@ import { genericUpdateController } from "@/src/controllers/api/genericUpdateCont import { getAllianceController } from "@/src/controllers/api/getAllianceController"; import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController"; import { getFriendsController } from "@/src/controllers/api/getFriendsController"; +import { getGuildContributionsController } from "@/src/controllers/api/getGuildContributionsController"; import { getGuildController } from "@/src/controllers/api/getGuildController"; import { getGuildDojoController } from "@/src/controllers/api/getGuildDojoController"; import { getGuildLogController } from "@/src/controllers/api/getGuildLogController"; @@ -139,6 +140,7 @@ apiRouter.get("/drones.php", dronesController); apiRouter.get("/getDailyDealStockLevels.php", getDailyDealStockLevelsController); apiRouter.get("/getFriends.php", getFriendsController); apiRouter.get("/getGuild.php", getGuildController); +apiRouter.get("/getGuildContributions.php", getGuildContributionsController); apiRouter.get("/getGuildDojo.php", getGuildDojoController); apiRouter.get("/getGuildLog.php", getGuildLogController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 0237cd2b..1d5dda15 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -58,12 +58,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s if (guildMember.accountId.equals(accountId)) { missingEntry = false; } else { - member.DisplayName = (await Account.findOne( - { - _id: guildMember.accountId - }, - "DisplayName" - ))!.DisplayName; + member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; await fillInInventoryDataForGuildMember(member); } members.push(member); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index da444a2b..ab4d1c06 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -88,6 +88,10 @@ export interface IGuildMemberDatabase { guildId: Types.ObjectId; status: number; rank: number; + RegularCreditsContributed?: number; + PremiumCreditsContributed?: number; + MiscItemsContributed?: IMiscItem[]; + ShipDecorationsContributed?: ITypeCount[]; } export interface IGuildMemberClient { From a622393933c8f99d5fa7602b3fdc95c54680d8fb Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Mar 2025 03:33:27 -0700 Subject: [PATCH 273/776] chore: don't validate Nonce in query (#1341) By asking MongoDB to simply find the account by the ID and then validating the nonce ourselves, we save roughly 1ms. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1341 --- src/services/loginService.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/services/loginService.ts b/src/services/loginService.ts index c69e891d..77ffc95c 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -74,11 +74,8 @@ export const getAccountForRequest = async (req: Request): Promise Date: Thu, 27 Mar 2025 03:33:39 -0700 Subject: [PATCH 274/776] chore: simplify logoutController (#1342) Reducing 3-4 MongoDB operations to only 1. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1342 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/logoutController.ts | 27 ++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/logoutController.ts b/src/controllers/api/logoutController.ts index f6f8863f..889e7d78 100644 --- a/src/controllers/api/logoutController.ts +++ b/src/controllers/api/logoutController.ts @@ -1,19 +1,28 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; import { Account } from "@/src/models/loginModel"; -const logoutController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); - const account = await Account.findById(accountId); - if (account) { - account.Nonce = 0; - await account.save(); +export const logoutController: RequestHandler = async (req, res) => { + if (!req.query.accountId) { + throw new Error("Request is missing accountId parameter"); } + const nonce: number = parseInt(req.query.nonce as string); + if (!nonce) { + throw new Error("Request is missing nonce parameter"); + } + + await Account.updateOne( + { + _id: req.query.accountId, + Nonce: nonce + }, + { + Nonce: 0 + } + ); + res.writeHead(200, { "Content-Type": "text/html", "Content-Length": 1 }); res.end("1"); }; - -export { logoutController }; From 2516af9acc0cb0ea8568d5041b16111e28141a64 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:30:12 +0100 Subject: [PATCH 275/776] chore: fix saveSettingsController --- src/controllers/api/saveSettingsController.ts | 4 ++-- src/models/inventoryModels/inventoryModel.ts | 3 ++- src/types/inventoryTypes/inventoryTypes.ts | 9 +++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/saveSettingsController.ts b/src/controllers/api/saveSettingsController.ts index 72bf8bfa..40780a35 100644 --- a/src/controllers/api/saveSettingsController.ts +++ b/src/controllers/api/saveSettingsController.ts @@ -14,9 +14,9 @@ const saveSettingsController: RequestHandler = async (req, res): Promise = const settingResults = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); - inventory.Settings = Object.assign(inventory.Settings, settingResults.Settings); + inventory.Settings = Object.assign(inventory.Settings ?? {}, settingResults.Settings); await inventory.save(); - res.json(inventory.Settings); + res.json({ Settings: inventory.Settings }); }; export { saveSettingsController }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 7904a12f..2f685233 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -496,7 +496,8 @@ const settingsSchema = new Schema({ GiftMode: String, GuildInvRestriction: String, ShowFriendInvNotifications: Boolean, - TradingRulesConfirmed: Boolean + TradingRulesConfirmed: Boolean, + SubscribedToSurveys: Boolean }); const consumedSchuitsSchema = new Schema( diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 7e18be4d..5d0edcfc 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -295,7 +295,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Nemesis?: INemesisClient; NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; - Settings: ISettings; + Settings?: ISettings; PersonalTechProjects: IPersonalTechProject[]; PlayerSkills: IPlayerSkills; CrewShipAmmo: ITypeCount[]; @@ -971,11 +971,12 @@ export interface ISentientSpawnChanceBoosters { } export interface ISettings { - FriendInvRestriction: string; - GiftMode: string; - GuildInvRestriction: string; + FriendInvRestriction: "GIFT_MODE_ALL" | "GIFT_MODE_FRIENDS" | "GIFT_MODE_NONE"; + GiftMode: "GIFT_MODE_ALL" | "GIFT_MODE_FRIENDS" | "GIFT_MODE_NONE"; + GuildInvRestriction: "GIFT_MODE_ALL" | "GIFT_MODE_FRIENDS" | "GIFT_MODE_NONE"; ShowFriendInvNotifications: boolean; TradingRulesConfirmed: boolean; + SubscribedToSurveys?: boolean; } export interface IShipInventory { From 2b9eb1844d7216dbb4acbcd2a7c995e98fb1552e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:49:26 +0100 Subject: [PATCH 276/776] chore: use inventory projection for saveSettingsController --- src/controllers/api/saveSettingsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/saveSettingsController.ts b/src/controllers/api/saveSettingsController.ts index 40780a35..15304626 100644 --- a/src/controllers/api/saveSettingsController.ts +++ b/src/controllers/api/saveSettingsController.ts @@ -13,7 +13,7 @@ const saveSettingsController: RequestHandler = async (req, res): Promise = const settingResults = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "Settings"); inventory.Settings = Object.assign(inventory.Settings ?? {}, settingResults.Settings); await inventory.save(); res.json({ Settings: inventory.Settings }); From ba795150a9ba2cad75a579fdea17478d410bbc49 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:45:33 +0100 Subject: [PATCH 277/776] chore: fix shape of RecentVendorPurchases in InventoryChanges --- src/services/purchaseService.ts | 22 ++++++++++------------ src/types/purchaseTypes.ts | 2 ++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index c17b562f..48069432 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -99,18 +99,16 @@ export const handlePurchase = async ( Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) }); } - prePurchaseInventoryChanges.RecentVendorPurchases = [ - { - VendorType: manifest.VendorInfo.TypeName, - PurchaseHistory: [ - { - ItemId: ItemId, - NumPurchased: numPurchased, - Expiry: offer.Expiry - } - ] - } - ]; + prePurchaseInventoryChanges.RecentVendorPurchases = { + VendorType: manifest.VendorInfo.TypeName, + PurchaseHistory: [ + { + ItemId: ItemId, + NumPurchased: numPurchased, + Expiry: offer.Expiry + } + ] + }; } purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; } else { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index c90e4588..f665f9da 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -5,6 +5,7 @@ import { IMiscItem, INemesisClient, ITypeCount, + IRecentVendorPurchaseClient, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; @@ -43,6 +44,7 @@ export type IInventoryChanges = { MiscItems?: IMiscItem[]; EmailItems?: ITypeCount[]; Nemesis?: Partial; + RecentVendorPurchases?: IRecentVendorPurchaseClient; } & Record< Exclude< string, From a56ff89bb93a37d17b94a3d5db2b076feab88816 Mon Sep 17 00:00:00 2001 From: Sainan Date: Thu, 27 Mar 2025 12:27:38 -0700 Subject: [PATCH 278/776] feat: equipment IsNew flag (#1309) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1309 --- src/models/inventoryModels/inventoryModel.ts | 3 ++- src/services/inventoryService.ts | 3 ++- src/services/saveLoadoutService.ts | 7 ++++++- src/types/inventoryTypes/commonInventoryTypes.ts | 1 + src/types/saveLoadoutTypes.ts | 6 +++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 2f685233..3abcfff5 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -849,7 +849,8 @@ const EquipmentSchema = new Schema( Customization: crewShipCustomizationSchema, RailjackImage: FlavourItemSchema, CrewMembers: crewShipMembersSchema, - Details: detailsSchema + Details: detailsSchema, + IsNew: Boolean }, { id: false } ); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 39361841..e294d9a3 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -907,7 +907,8 @@ export const addEquipment = ( ItemType: type, Configs: [], XP: 0, - ModularParts: modularParts + ModularParts: modularParts, + IsNew: true }, defaultOverwrites ); diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 63533e43..ff0159f5 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -155,7 +155,12 @@ export const handleInventoryItemConfigChange = async ( } for (const [configId, config] of Object.entries(itemConfigEntries)) { - inventoryItem.Configs[parseInt(configId)] = config; + if (typeof config !== "boolean") { + inventoryItem.Configs[parseInt(configId)] = config; + } + } + if ("IsNew" in itemConfigEntries) { + inventoryItem.IsNew = itemConfigEntries.IsNew; } } break; diff --git a/src/types/inventoryTypes/commonInventoryTypes.ts b/src/types/inventoryTypes/commonInventoryTypes.ts index 13987e80..ea26992a 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -140,6 +140,7 @@ export interface IEquipmentDatabase { RailjackImage?: IFlavourItem; CrewMembers?: ICrewShipMembersDatabase; Details?: IKubrowPetDetailsDatabase; + IsNew?: boolean; _id: Types.ObjectId; } diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index e04ec6a2..8a1b8883 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -48,9 +48,9 @@ export interface IItemEntry { [itemId: string]: IConfigEntry; } -export interface IConfigEntry { - [configId: string]: IItemConfig; -} +export type IConfigEntry = { + [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; +} & { IsNew?: boolean }; export interface ILoadoutClient extends Omit {} From 36d2b2dda543d283338472ea9dd56093980c00e2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:57:44 -0700 Subject: [PATCH 279/776] feat: gifting (#1344) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1344 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/giftingController.ts | 92 ++++++++++++++++++++++ src/controllers/api/inboxController.ts | 55 ++++++++++--- src/controllers/api/inventoryController.ts | 1 + src/models/inboxModel.ts | 14 ++++ src/routes/api.ts | 2 + src/services/loginService.ts | 4 + 6 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 src/controllers/api/giftingController.ts diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts new file mode 100644 index 00000000..ce3e5a30 --- /dev/null +++ b/src/controllers/api/giftingController.ts @@ -0,0 +1,92 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account } from "@/src/models/loginModel"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const giftingController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) { + throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`); + } + + const account = await Account.findOne( + data.RecipientId ? { _id: data.RecipientId.$oid } : { DisplayName: data.Recipient } + ); + if (!account) { + res.status(400).send("9").end(); + return; + } + const inventory = await getInventory(account._id.toString(), "Suits Settings"); + + // Cannot gift items to players that have not completed the tutorial. + if (inventory.Suits.length == 0) { + res.status(400).send("14").end(); + return; + } + + // Cannot gift to players who have gifting disabled. + // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented + if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") { + res.status(400).send("17").end(); + return; + } + + // TODO: Cannot gift items with mastery requirement to players who are too low level. (Code 2) + // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) + // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20) + + const senderAccount = await getAccountForRequest(req); + const senderInventory = await getInventory( + senderAccount._id.toString(), + "PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining" + ); + + if (senderInventory.GiftsRemaining == 0) { + res.status(400).send("10").end(); + return; + } + senderInventory.GiftsRemaining -= 1; + + updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true); + await senderInventory.save(); + + const senderName = getSuffixedName(senderAccount); + await createMessage(account._id.toString(), [ + { + sndr: senderName, + msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage", + arg: [ + { + Key: "GIFTER_NAME", + Tag: senderName + }, + { + Key: "GIFT_QUANTITY", + Tag: data.PurchaseParams.Quantity + } + ], + sub: "/Lotus/Language/Menu/GiftReceivedSubject", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + gifts: [ + { + GiftType: data.PurchaseParams.StoreItem + } + ] + } + ]); + + res.end(); +}; + +interface IGiftingRequest { + PurchaseParams: IPurchaseParams; + Message?: string; + Recipient?: string; + RecipientId?: IOid; + buildLabel: string; +} diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 896c01b6..84ebf03e 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -1,21 +1,24 @@ import { RequestHandler } from "express"; import { Inbox } from "@/src/models/inboxModel"; import { + createMessage, createNewEventMessages, deleteAllMessagesRead, deleteMessageRead, getAllMessagesSorted, getMessage } from "@/src/services/inboxService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addItems, getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; +import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; -import { ExportGear } from "warframe-public-export-plus"; +import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; export const inboxController: RequestHandler = async (req, res) => { const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); + const accountId = account._id.toString(); if (deleteId) { if (deleteId === "DeleteAllRead") { @@ -29,12 +32,12 @@ export const inboxController: RequestHandler = async (req, res) => { } else if (messageId) { const message = await getMessage(messageId as string); message.r = true; + await message.save(); + const attachmentItems = message.att; const attachmentCountedItems = message.countedAtt; - if (!attachmentItems && !attachmentCountedItems) { - await message.save(); - + if (!attachmentItems && !attachmentCountedItems && !message.gifts) { res.status(200).end(); return; } @@ -54,9 +57,43 @@ export const inboxController: RequestHandler = async (req, res) => { if (attachmentCountedItems) { await addItems(inventory, attachmentCountedItems, inventoryChanges); } + if (message.gifts) { + const sender = await getAccountFromSuffixedName(message.sndr); + const recipientName = getSuffixedName(account); + const giftQuantity = message.arg!.find(x => x.Key == "GIFT_QUANTITY")!.Tag as number; + for (const gift of message.gifts) { + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges + ); + if (sender) { + await createMessage(sender._id.toString(), [ + { + sndr: recipientName, + msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody", + arg: [ + { + Key: "RECIPIENT_NAME", + Tag: recipientName + }, + { + Key: "GIFT_TYPE", + Tag: gift.GiftType + }, + { + Key: "GIFT_QUANTITY", + Tag: giftQuantity + } + ], + sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject", + icon: ExportFlavour[inventory.ActiveAvatarImageType].icon, + highPriority: true + } + ]); + } + } + } await inventory.save(); - await message.save(); - res.json({ InventoryChanges: inventoryChanges }); } else if (latestClientMessageId) { await createNewEventMessages(req); diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 36b1221d..e2f4a99d 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -33,6 +33,7 @@ export const inventoryController: RequestHandler = async (request, response) => inventory[key] = 16000 + inventory.PlayerLevel * 500; } inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; + inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index c3ad8add..c2d8af44 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -31,6 +31,7 @@ export interface IMessage { countedAtt?: ITypeCount[]; transmission?: string; arg?: Arg[]; + gifts?: IGift[]; r?: boolean; contextInfo?: string; acceptAction?: string; @@ -43,6 +44,10 @@ export interface Arg { Tag: string | number; } +export interface IGift { + GiftType: string; +} + //types are wrong // export interface IMessageDatabase { // _id: Types.ObjectId; @@ -80,6 +85,14 @@ export interface Arg { // cinematic: string; // requiredLevel: string; // } + +const giftSchema = new Schema( + { + GiftType: String + }, + { _id: false } +); + const messageSchema = new Schema( { ownerId: Schema.Types.ObjectId, @@ -93,6 +106,7 @@ const messageSchema = new Schema( endDate: Date, r: Boolean, att: { type: [String], default: undefined }, + gifts: { type: [giftSchema], default: undefined }, countedAtt: { type: [typeCountSchema], default: undefined }, transmission: String, arg: { diff --git a/src/routes/api.ts b/src/routes/api.ts index 30d93d0e..8fd82e30 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -51,6 +51,7 @@ import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSe import { getShipController } from "@/src/controllers/api/getShipController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; +import { giftingController } from "@/src/controllers/api/giftingController"; import { gildWeaponController } from "@/src/controllers/api/gildWeaponController"; import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; @@ -203,6 +204,7 @@ apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getFriends.php", getFriendsController); apiRouter.post("/getGuildDojo.php", getGuildDojoController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); +apiRouter.post("/gifting.php", giftingController); apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 77ffc95c..df9e6c90 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -100,3 +100,7 @@ export const getSuffixedName = (account: TAccountDocument): string => { const suffix = ((crc32.str(name.toLowerCase() + "595") >>> 0) + platform_magics[platformId]) % 1000; return name + "#" + suffix.toString().padStart(3, "0"); }; + +export const getAccountFromSuffixedName = (name: string): Promise => { + return Account.findOne({ DisplayName: name.split("#")[0] }); +}; From 692dfaf0a59a8655add50dd5872e4ba95961cc70 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:57:57 -0700 Subject: [PATCH 280/776] feat: respect Settings.GuildInvRestriction for addToGuild (#1345) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1345 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/addToGuildController.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index ee1c0fc4..5d509a46 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -18,6 +18,13 @@ export const addToGuildController: RequestHandler = async (req, res) => { return; } + const inventory = await getInventory(account._id.toString(), "Settings"); + // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented + if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") { + res.status(400).json("Invite restricted"); + return; + } + const guild = (await Guild.findById(payload.GuildId.$oid, "Name"))!; const senderAccount = await getAccountForRequest(req); if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { From aad3a7bcf7faacefe88c5e1de03e4efff4b4fed0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 00:17:33 +0100 Subject: [PATCH 281/776] chore: update vendor purchase response --- src/services/purchaseService.ts | 3 ++- src/types/purchaseTypes.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 48069432..7f0170d2 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -99,7 +99,7 @@ export const handlePurchase = async ( Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) }); } - prePurchaseInventoryChanges.RecentVendorPurchases = { + prePurchaseInventoryChanges.NewVendorPurchase = { VendorType: manifest.VendorInfo.TypeName, PurchaseHistory: [ { @@ -109,6 +109,7 @@ export const handlePurchase = async ( } ] }; + prePurchaseInventoryChanges.RecentVendorPurchases = prePurchaseInventoryChanges.NewVendorPurchase; } purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; } else { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index f665f9da..51459454 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -44,7 +44,8 @@ export type IInventoryChanges = { MiscItems?: IMiscItem[]; EmailItems?: ITypeCount[]; Nemesis?: Partial; - RecentVendorPurchases?: IRecentVendorPurchaseClient; + NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 + RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 } & Record< Exclude< string, From 24c288fe61625fb75a32158c7a2061853dd3b5f0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 01:02:06 +0100 Subject: [PATCH 282/776] chore: handle email address starting with @ --- src/controllers/api/loginController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 19b3c380..af7beb55 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -24,7 +24,7 @@ export const loginController: RequestHandler = async (request, response) => { if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { try { const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); - let name = nameFromEmail || "SpaceNinja"; + let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja"; if (await isNameTaken(name)) { let suffix = 0; do { From b14927d6059897d2b9f8b56ba1b34e63c39f7c07 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:07:30 -0700 Subject: [PATCH 283/776] fix: handle recipes requiring non-MiscItem items (#1348) e.g. mutagens and antigens require vome and fass residue which are consumables Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1348 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/claimCompletedRecipeController.ts | 24 +++++++++---------- src/controllers/api/startRecipeController.ts | 9 ++----- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index b796b53b..e8c1b4fb 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -11,13 +11,13 @@ import { getInventory, updateCurrency, addItem, - addMiscItems, addRecipes, - occupySlot + occupySlot, + combineInventoryChanges } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -51,14 +51,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ...updateCurrency(inventory, recipe.buildPrice * -1, false) }; - const nonMiscItemIngredients = new Set(); + const equipmentIngredients = new Set(); for (const category of ["LongGuns", "Pistols", "Melee"] as const) { if (pendingRecipe[category]) { pendingRecipe[category].forEach(item => { const index = inventory[category].push(item) - 1; inventoryChanges[category] ??= []; inventoryChanges[category].push(inventory[category][index].toJSON()); - nonMiscItemIngredients.add(item.ItemType); + equipmentIngredients.add(item.ItemType); occupySlot(inventory, InventorySlot.WEAPONS, false); inventoryChanges.WeaponBin ??= { Slots: 0 }; @@ -66,14 +66,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = }); } } - const miscItemChanges: IMiscItem[] = []; - recipe.ingredients.forEach(ingredient => { - if (!nonMiscItemIngredients.has(ingredient.ItemType)) { - miscItemChanges.push(ingredient); + for (const ingredient of recipe.ingredients) { + if (!equipmentIngredients.has(ingredient.ItemType)) { + combineInventoryChanges( + inventoryChanges, + await addItem(inventory, ingredient.ItemType, ingredient.ItemCount) + ); } - }); - addMiscItems(inventory, miscItemChanges); - inventoryChanges.MiscItems = miscItemChanges; + } await inventory.save(); res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index 0b8c4667..efdcb292 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -3,7 +3,7 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { getRecipe } from "@/src/services/itemDataService"; -import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { Types } from "mongoose"; import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -55,12 +55,7 @@ export const startRecipeController: RequestHandler = async (req, res) => { inventory[category].splice(equipmentIndex, 1); freeUpSlot(inventory, InventorySlot.WEAPONS); } else { - addMiscItems(inventory, [ - { - ItemType: recipe.ingredients[i].ItemType, - ItemCount: recipe.ingredients[i].ItemCount * -1 - } - ]); + await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); } } From eb332d5e32ded9b073bc80293f7c8ce335792cc4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:07:39 -0700 Subject: [PATCH 284/776] feat(webui): ability to add mutagens and antigens via "add items" (#1349) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1349 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 1c5a0cba..accbac1c 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -74,7 +74,8 @@ const getItemListsController: RequestHandler = (req, response) => { uniqueName.split("/")[5] == "HoverboardParts" || uniqueName.split("/")[5] == "ModularMelee01" || uniqueName.split("/")[5] == "ModularMelee02" || - uniqueName.split("/")[5] == "ModularMeleeInfested" + uniqueName.split("/")[5] == "ModularMeleeInfested" || + uniqueName.split("/")[6] == "CreaturePetParts" ) { res.ModularParts.push({ uniqueName, From ae5a540975a072a19aa3cd7e45ea6ef4830a05a1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:08:02 -0700 Subject: [PATCH 285/776] feat: crafting infested cats and dogs (#1352) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1352 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/modularWeaponCraftingController.ts | 78 +++++++++++++++++-- src/helpers/modularWeaponHelper.ts | 8 +- src/models/inventoryModels/inventoryModel.ts | 4 +- src/types/inventoryTypes/inventoryTypes.ts | 8 +- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 2b116a8d..37d7e950 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -8,11 +8,17 @@ import { addMiscItems, applyDefaultUpgrades, occupySlot, - productCategoryToInventoryBin + productCategoryToInventoryBin, + combineInventoryChanges, + addSpecialItem } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { getRandomInt } from "@/src/services/rngService"; +import { ExportSentinels } from "warframe-public-export-plus"; +import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { WeaponType: string; @@ -29,11 +35,67 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const inventory = await getInventory(accountId); const defaultUpgrades = getDefaultUpgrades(data.Parts); - const configs = applyDefaultUpgrades(inventory, defaultUpgrades); - const inventoryChanges: IInventoryChanges = { - ...addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }), - ...occupySlot(inventory, productCategoryToInventoryBin(category)!, false) + const defaultOverwrites: Partial = { + Configs: applyDefaultUpgrades(inventory, defaultUpgrades) }; + const inventoryChanges: IInventoryChanges = {}; + if (category == "KubrowPets") { + const traits = + data.WeaponType.indexOf("Catbrow") != -1 + ? { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: { + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": + "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadC", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": + "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadB", + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": + "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadA" + }[data.WeaponType] + } + : { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: { + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": + "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadA", + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": + "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadB", + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": + "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadC" + }[data.WeaponType] + }; + defaultOverwrites.Details = { + HasCollar: true, + Status: Status.StatusStasis, + IsMale: !!getRandomInt(0, 1), + Size: 0.7 + Math.random() * 0.3, + DominantTraits: traits, + RecessiveTraits: traits + }; + + // Only save mutagen & antigen in the ModularParts. + defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]]; + + for (const specialItem of ExportSentinels[data.WeaponType].exalted!) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + } + addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); + combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false)); if (defaultUpgrades) { inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); } @@ -48,7 +110,11 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) } const currencyChanges = updateCurrency( inventory, - category == "Hoverboards" || category == "MoaPets" || category == "LongGuns" || category == "Pistols" + category == "Hoverboards" || + category == "MoaPets" || + category == "LongGuns" || + category == "Pistols" || + category == "KubrowPets" ? 5000 : 4000, // Definitely correct for Melee & OperatorAmps false diff --git a/src/helpers/modularWeaponHelper.ts b/src/helpers/modularWeaponHelper.ts index 82610000..cf0e9b19 100644 --- a/src/helpers/modularWeaponHelper.ts +++ b/src/helpers/modularWeaponHelper.ts @@ -15,5 +15,11 @@ export const modularWeaponTypes: Record = { "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets" + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": "KubrowPets" }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 3abcfff5..cb5795be 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -815,7 +815,9 @@ detailsSchema.set("toJSON", { const db = returnedObject as IKubrowPetDetailsDatabase; const client = returnedObject as IKubrowPetDetailsClient; - client.HatchDate = toMongoDate(db.HatchDate); + if (db.HatchDate) { + client.HatchDate = toMongoDate(db.HatchDate); + } } }); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 5d0edcfc..a003c930 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -679,12 +679,12 @@ export enum KubrowPetPrintItemType { } export interface IKubrowPetDetailsDatabase { - Name: string; - IsPuppy: boolean; + Name?: string; + IsPuppy?: boolean; HasCollar: boolean; - PrintsRemaining: number; + PrintsRemaining?: number; Status: Status; - HatchDate: Date; + HatchDate?: Date; DominantTraits: ITraits; RecessiveTraits: ITraits; IsMale: boolean; From aa7d5067bc26740002c2616589adfc58f7914644 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:08:22 -0700 Subject: [PATCH 286/776] feat: retrievePetFromStasis (#1354) Closes #621 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1354 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/retrievePetFromStasisController.ts | 33 +++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 src/controllers/api/retrievePetFromStasisController.ts diff --git a/src/controllers/api/retrievePetFromStasisController.ts b/src/controllers/api/retrievePetFromStasisController.ts new file mode 100644 index 00000000..8547f7f4 --- /dev/null +++ b/src/controllers/api/retrievePetFromStasisController.ts @@ -0,0 +1,33 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const retrievePetFromStasisController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets"); + const data = getJSONfromString(String(req.body)); + + let oldPetId: string | undefined; + for (const pet of inventory.KubrowPets) { + if (pet.Details!.Status == Status.StatusAvailable) { + pet.Details!.Status = Status.StatusStasis; + oldPetId = pet._id.toString(); + break; + } + } + + inventory.KubrowPets.id(data.petId)!.Details!.Status = Status.StatusAvailable; + + await inventory.save(); + res.json({ + petId: data.petId, + oldPetId, + status: Status.StatusAvailable + }); +}; + +interface IRetrievePetFromStasisRequest { + petId: string; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 8fd82e30..735a48a9 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -85,6 +85,7 @@ import { queueDojoComponentDestructionController } from "@/src/controllers/api/q import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; +import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadout"; import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; @@ -230,6 +231,7 @@ apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); +apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveLoadout.php", saveLoadoutController); apiRouter.post("/saveSettings.php", saveSettingsController); From 92e647a0fd01f7a5455d89ef0613706141fca807 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:53:27 +0100 Subject: [PATCH 287/776] chore: fill out all details for infested pets --- src/controllers/api/modularWeaponCraftingController.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 37d7e950..9866b7b4 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -79,10 +79,14 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) }[data.WeaponType] }; defaultOverwrites.Details = { + Name: "", + IsPuppy: false, HasCollar: true, + PrintsRemaining: 2, Status: Status.StatusStasis, + HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000), IsMale: !!getRandomInt(0, 1), - Size: 0.7 + Math.random() * 0.3, + Size: getRandomInt(70, 100) / 100, DominantTraits: traits, RecessiveTraits: traits }; From 212b7b1ce92dbe8b95e28fa1b76601b536d4f924 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 28 Mar 2025 06:49:19 -0700 Subject: [PATCH 288/776] chore(webui): kDrive typo (#1357) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1357 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index 530e8091..8697414e 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -209,7 +209,7 @@ function fetchItemList() { "/Lotus/Weapons/Operator/Pistols/DrifterPistol/DrifterPistolPlayerWeapon": { name: loc("code_sirocco") }, - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kdrive") } + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kDrive") } }; for (const [type, items] of Object.entries(data)) { if (type == "archonCrystalUpgrades") { From 3a904753f2067c7192b1a050b76e41744e03caa7 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 28 Mar 2025 06:49:29 -0700 Subject: [PATCH 289/776] chore: accurate infested pet traits (#1356) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1356 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../api/modularWeaponCraftingController.ts | 111 ++++++++++++------ 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 9866b7b4..bab27b23 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -40,44 +40,79 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) }; const inventoryChanges: IInventoryChanges = {}; if (category == "KubrowPets") { - const traits = - data.WeaponType.indexOf("Catbrow") != -1 - ? { - BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareBase", - SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareSecondary", - TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareTertiary", - AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareAccent", - EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareEyes", - FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", - Personality: data.WeaponType, - BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", - Head: { - "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": - "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadC", - "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": - "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadB", - "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": - "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadA" - }[data.WeaponType] - } - : { - BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareBase", - SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareSecondary", - TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareTertiary", - AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareAccent", - EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareEyes", - FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", - Personality: data.WeaponType, - BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", - Head: { - "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": - "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadA", - "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": - "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadB", - "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": - "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadC" - }[data.WeaponType] - }; + const traits = { + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadC" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadB" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadA" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadB" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadC" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadA" + } + }[data.WeaponType]; + + if (!traits) { + throw new Error(`unknown KubrowPets type: ${data.WeaponType}`); + } + defaultOverwrites.Details = { Name: "", IsPuppy: false, From dcc2b903ac0832f648de238a94917ff2c13714b1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:50:44 -0700 Subject: [PATCH 290/776] feat: maturePet (#1355) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1355 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/maturePetController.ts | 27 ++++++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 src/controllers/api/maturePetController.ts diff --git a/src/controllers/api/maturePetController.ts b/src/controllers/api/maturePetController.ts new file mode 100644 index 00000000..1bfb83f6 --- /dev/null +++ b/src/controllers/api/maturePetController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const maturePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets"); + const data = getJSONfromString(String(req.body)); + const details = inventory.KubrowPets.id(data.petId)!.Details!; + details.IsPuppy = data.revert; + await inventory.save(); + res.json({ + petId: data.petId, + updateCollar: true, + armorSkins: ["", "", ""], + furPatterns: data.revert + ? ["", "", ""] + : [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern], + unmature: data.revert + }); +}; + +interface IMaturePetRequest { + petId: string; + revert: boolean; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 735a48a9..bb1404f5 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -72,6 +72,7 @@ import { loginRewardsController } from "@/src/controllers/api/loginRewardsContro import { loginRewardsSelectionController } from "@/src/controllers/api/loginRewardsSelectionController"; import { logoutController } from "@/src/controllers/api/logoutController"; import { marketRecommendationsController } from "@/src/controllers/api/marketRecommendationsController"; +import { maturePetController } from "@/src/controllers/api/maturePetController"; import { missionInventoryUpdateController } from "@/src/controllers/api/missionInventoryUpdateController"; import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController"; import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; @@ -219,6 +220,7 @@ apiRouter.post("/inventorySlots.php", inventorySlotsController); apiRouter.post("/joinSession.php", joinSessionController); apiRouter.post("/login.php", loginController); apiRouter.post("/loginRewardsSelection.php", loginRewardsSelectionController); +apiRouter.post("/maturePet.php", maturePetController); apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); From 30ae95bec80f04694eef0bf4c8385d64b761dc98 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:24:26 +0100 Subject: [PATCH 291/776] fix: insufficient guild projection in addToGuildController --- src/controllers/api/addToGuildController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index 5d509a46..41a6e227 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -25,7 +25,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { return; } - const guild = (await Guild.findById(payload.GuildId.$oid, "Name"))!; + const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; const senderAccount = await getAccountForRequest(req); if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { res.status(400).json("Invalid permission"); From a167216730a6b9a81bc59b2b219662b203a8e769 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 09:27:38 -0700 Subject: [PATCH 292/776] feat: WeaponSkins IsNew flag (#1347) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1347 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 3 ++- src/services/inventoryService.ts | 2 +- src/services/saveLoadoutService.ts | 7 +++++++ src/types/inventoryTypes/inventoryTypes.ts | 1 + src/types/saveLoadoutTypes.ts | 1 + 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index cb5795be..794e9d2a 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -586,7 +586,8 @@ const spectreLoadoutsSchema = new Schema( const weaponSkinsSchema = new Schema( { - ItemType: String + ItemType: String, + IsNew: Boolean }, { id: false } ); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index e294d9a3..9bc65bc9 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -938,7 +938,7 @@ export const addSkin = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.WeaponSkins.push({ ItemType: typeName }) - 1; + const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.WeaponSkins ??= []; (inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push( diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index ff0159f5..d1fc0fcf 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -140,6 +140,13 @@ export const handleInventoryItemConfigChange = async ( inventory.UseAdultOperatorLoadout = equipment as boolean; break; } + case "WeaponSkins": { + const itemEntries = equipment as IItemEntry; + for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) { + inventory.WeaponSkins.id(itemId)!.IsNew = itemConfigEntries.IsNew; + } + break; + } default: { if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") { logger.debug(`general Item config saved of type ${equipmentName}`, { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index a003c930..16e230c8 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1017,6 +1017,7 @@ export interface ITaunt { export interface IWeaponSkinDatabase { ItemType: string; + IsNew?: boolean; _id: Types.ObjectId; } diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index 8a1b8883..3bef13a8 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -36,6 +36,7 @@ export interface ISaveLoadoutRequest { EquippedGear: string[]; EquippedEmotes: string[]; UseAdultOperatorLoadout: boolean; + WeaponSkins: IItemEntry; } export interface ISaveLoadoutRequestNoUpgradeVer extends Omit {} From 6a1e508109de0c421afaaafb84cdb7b1c0d28bcb Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 09:27:56 -0700 Subject: [PATCH 293/776] feat: initial vendor rotations (#1360) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1360 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/getVendorInfoController.ts | 4 +- src/services/leaderboardService.ts | 3 +- src/services/purchaseService.ts | 27 ++++++-- src/services/serversideVendorsService.ts | 64 ++++++++++++------- src/types/vendorTypes.ts | 46 +++++++++++++ .../GuildAdvertisementVendorManifest.json | 52 +++++++++++---- .../HubsIronwakeDondaVendorManifest.json | 12 ++-- .../TeshinHardModeVendorManifest.json | 6 +- 8 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 src/types/vendorTypes.ts diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts index b161176e..c7212550 100644 --- a/src/controllers/api/getVendorInfoController.ts +++ b/src/controllers/api/getVendorInfoController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; export const getVendorInfoController: RequestHandler = (req, res) => { if (typeof req.query.vendor == "string") { @@ -7,7 +7,7 @@ export const getVendorInfoController: RequestHandler = (req, res) => { if (!manifest) { throw new Error(`Unknown vendor: ${req.query.vendor}`); } - res.json(manifest); + res.json(preprocessVendorManifest(manifest)); } else { res.status(400).end(); } diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts index c4084d6e..b0e03518 100644 --- a/src/services/leaderboardService.ts +++ b/src/services/leaderboardService.ts @@ -15,8 +15,7 @@ export const submitLeaderboardScore = async ( expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); } else { const EPOCH = 1734307200 * 1000; // Monday - const day = Math.trunc((Date.now() - EPOCH) / 86400000); - const week = Math.trunc(day / 7); + const week = Math.trunc((Date.now() - EPOCH) / 604800000); const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; expiry = new Date(weekEnd); diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 7f0170d2..f2b71427 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -9,7 +9,7 @@ import { updateSlots } from "@/src/services/inventoryService"; import { getRandomWeightedRewardUc } from "@/src/services/rngService"; -import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByOid, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; @@ -52,8 +52,9 @@ export const handlePurchase = async ( const prePurchaseInventoryChanges: IInventoryChanges = {}; if (purchaseRequest.PurchaseParams.Source == 7) { - const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); - if (manifest) { + const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); + if (rawManifest) { + const manifest = preprocessVendorManifest(rawManifest); let ItemId: string | undefined; if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) @@ -87,16 +88,28 @@ export const handlePurchase = async ( }) - 1 ]; } + let expiry = parseInt(offer.Expiry.$date.$numberLong); + if (purchaseRequest.PurchaseParams.IsWeekly) { + const EPOCH = 1734307200 * 1000; // Monday + const week = Math.trunc((Date.now() - EPOCH) / 604800000); + const weekStart = EPOCH + week * 604800000; + expiry = weekStart + 604800000; + } const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); let numPurchased = purchaseRequest.PurchaseParams.Quantity; if (historyEntry) { - numPurchased += historyEntry.NumPurchased; - historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; + if (Date.now() >= historyEntry.Expiry.getTime()) { + historyEntry.NumPurchased = numPurchased; + historyEntry.Expiry = new Date(expiry); + } else { + numPurchased += historyEntry.NumPurchased; + historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; + } } else { vendorPurchases.PurchaseHistory.push({ ItemId: ItemId, NumPurchased: purchaseRequest.PurchaseParams.Quantity, - Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) + Expiry: new Date(expiry) }); } prePurchaseInventoryChanges.NewVendorPurchase = { @@ -105,7 +118,7 @@ export const handlePurchase = async ( { ItemId: ItemId, NumPurchased: numPurchased, - Expiry: offer.Expiry + Expiry: { $date: { $numberLong: expiry.toString() } } } ] }; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 79e22ef3..37a3425c 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,4 +1,6 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { CRng, mixSeeds } from "@/src/services/rngService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; @@ -31,25 +33,6 @@ import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorI import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; -interface IVendorManifest { - VendorInfo: { - _id: IOid; - TypeName: string; - ItemManifest: { - StoreItem: string; - ItemPrices?: { ItemType: string; ItemCount: number; ProductCategory: string }[]; - Bin: string; - QuantityMultiplier: number; - Expiry: IMongoDate; - PurchaseQuantityLimit?: number; - RotatedWeekly?: boolean; - AllowMultipurchase: boolean; - Id: IOid; - }[]; - Expiry: IMongoDate; - }; -} - const vendorManifests: IVendorManifest[] = [ ArchimedeanVendorManifest, DeimosEntratiFragmentVendorProductsManifest, @@ -65,8 +48,8 @@ const vendorManifests: IVendorManifest[] = [ DuviriAcrithisVendorManifest, EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabVendorManifest, - GuildAdvertisementVendorManifest, - HubsIronwakeDondaVendorManifest, + GuildAdvertisementVendorManifest, // uses preprocessing + HubsIronwakeDondaVendorManifest, // uses preprocessing HubsPerrinSequenceWeaponVendorManifest, HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, @@ -79,7 +62,7 @@ const vendorManifests: IVendorManifest[] = [ SolarisDebtTokenVendorRepossessionsManifest, SolarisFishmongerVendorManifest, SolarisProspectorVendorManifest, - TeshinHardModeVendorManifest, + TeshinHardModeVendorManifest, // uses preprocessing ZarimanCommisionsManifestArchimedean ]; @@ -100,3 +83,38 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined } return undefined; }; + +export const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifestPreprocessed => { + if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { + const manifest = structuredClone(originalManifest); + const info = manifest.VendorInfo; + refreshExpiry(info.Expiry); + for (const offer of info.ItemManifest) { + const iteration = refreshExpiry(offer.Expiry); + if (offer.ItemPrices) { + for (const price of offer.ItemPrices) { + if (typeof price.ItemType != "string") { + const itemSeed = parseInt(offer.Id.$oid.substring(16), 16); + const rng = new CRng(mixSeeds(itemSeed, iteration)); + price.ItemType = rng.randomElement(price.ItemType); + } + } + } + } + return manifest as IVendorManifestPreprocessed; + } + return originalManifest as IVendorManifestPreprocessed; +}; + +const refreshExpiry = (expiry: IMongoDate): number => { + const period = parseInt(expiry.$date.$numberLong); + if (Date.now() >= period) { + const epoch = 1734307200 * 1000; // Monday (for weekly schedules) + const iteration = Math.trunc((Date.now() - epoch) / period); + const start = epoch + iteration * period; + const end = start + period; + expiry.$date.$numberLong = end.toString(); + return iteration; + } + return 0; +}; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts new file mode 100644 index 00000000..d7dbd749 --- /dev/null +++ b/src/types/vendorTypes.ts @@ -0,0 +1,46 @@ +import { IMongoDate, IOid } from "./commonTypes"; + +interface IItemPrice { + ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. + ItemCount: number; + ProductCategory: string; +} + +interface IItemPricePreprocessed extends Omit { + ItemType: string; +} + +interface IItemManifest { + StoreItem: string; + ItemPrices?: IItemPrice[]; + Bin: string; + QuantityMultiplier: number; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. + PurchaseQuantityLimit?: number; + RotatedWeekly?: boolean; + AllowMultipurchase: boolean; + Id: IOid; +} + +interface IItemManifestPreprocessed extends Omit { + ItemPrices?: IItemPricePreprocessed[]; +} + +interface IVendorInfo { + _id: IOid; + TypeName: string; + ItemManifest: IItemManifest[]; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. +} + +interface IVendorInfoPreprocessed extends Omit { + ItemManifest: IItemManifestPreprocessed[]; +} + +export interface IVendorManifest { + VendorInfo: IVendorInfo; +} + +export interface IVendorManifestPreprocessed { + VendorInfo: IVendorInfoPreprocessed; +} diff --git a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json index 05681d38..20e3e3a3 100644 --- a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json @@ -5,11 +5,17 @@ "ItemManifest": [ { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 12, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 12, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_4", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 79554843, @@ -17,11 +23,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 7, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 7, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_3", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 2413820225, @@ -29,11 +41,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 3, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 3, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_2", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 3262300883, @@ -41,11 +59,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 20, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 20, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_1", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 2797325750, @@ -53,11 +77,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 10, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 10, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_0", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 554932310, @@ -66,6 +96,6 @@ ], "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA", "RandomSeedType": "VRST_FLAVOUR_TEXT", - "Expiry": { "$date": { "$numberLong": "9999999000000" } } + "Expiry": { "$date": { "$numberLong": "604800000" } } } } diff --git a/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json index 0dabeb95..bec20cc1 100644 --- a/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json @@ -18,7 +18,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "AllowMultipurchase": true, @@ -39,7 +39,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -61,7 +61,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -83,7 +83,7 @@ "QuantityMultiplier": 35000, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -105,7 +105,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -118,7 +118,7 @@ "PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621", "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } } } diff --git a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json index 4572855f..7934f0a3 100644 --- a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json @@ -561,7 +561,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 25, @@ -583,7 +583,7 @@ "QuantityMultiplier": 10000, "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 25, @@ -596,7 +596,7 @@ "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D", "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } } } From e266f9e36c8907c25dd2d14be725f92f9107f2a0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 09:28:12 -0700 Subject: [PATCH 294/776] fix: save login reward was claimed on milestone days (#1367) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1367 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/loginRewardsController.ts | 8 ++++++-- .../api/loginRewardsSelectionController.ts | 12 ++++++++++-- src/services/loginRewardService.ts | 10 +++++----- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/loginRewardsController.ts b/src/controllers/api/loginRewardsController.ts index 16d77261..5280b77f 100644 --- a/src/controllers/api/loginRewardsController.ts +++ b/src/controllers/api/loginRewardsController.ts @@ -4,7 +4,8 @@ import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse, - isLoginRewardAChoice + isLoginRewardAChoice, + setAccountGotLoginRewardToday } from "@/src/services/loginRewardService"; import { getInventory } from "@/src/services/inventoryService"; @@ -44,8 +45,11 @@ export const loginRewardsController: RequestHandler = async (req, res) => { if (!isMilestoneDay && randomRewards.length == 1) { response.DailyTributeInfo.HasChosenReward = true; response.DailyTributeInfo.ChosenReward = randomRewards[0]; - response.DailyTributeInfo.NewInventory = await claimLoginReward(account, inventory, randomRewards[0]); + response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); await inventory.save(); + + setAccountGotLoginRewardToday(account); + await account.save(); } res.json(response); }; diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts index 8bd14dd7..4b6fc210 100644 --- a/src/controllers/api/loginRewardsSelectionController.ts +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -1,5 +1,9 @@ import { getInventory } from "@/src/services/inventoryService"; -import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService"; +import { + claimLoginReward, + getRandomLoginRewards, + setAccountGotLoginRewardToday +} from "@/src/services/loginRewardService"; import { getAccountForRequest } from "@/src/services/loginService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; @@ -28,9 +32,13 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res) } else { const randomRewards = getRandomLoginRewards(account, inventory); chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; - inventoryChanges = await claimLoginReward(account, inventory, chosenReward); + inventoryChanges = await claimLoginReward(inventory, chosenReward); } await inventory.save(); + + setAccountGotLoginRewardToday(account); + await account.save(); + res.json({ DailyTributeInfo: { NewInventory: inventoryChanges, diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index f54499e5..d32789b6 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -121,14 +121,9 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab }; export const claimLoginReward = async ( - account: TAccountDocument, inventory: TInventoryDatabaseDocument, reward: ILoginReward ): Promise => { - account.LoginDays += 1; - account.LastLoginRewardDate = Math.trunc(Date.now() / 86400000) * 86400; - await account.save(); - switch (reward.RewardType) { case "RT_RESOURCE": case "RT_STORE_ITEM": @@ -150,3 +145,8 @@ export const claimLoginReward = async ( } throw new Error(`unknown login reward type: ${reward.RewardType}`); }; + +export const setAccountGotLoginRewardToday = (account: TAccountDocument): void => { + account.LoginDays += 1; + account.LastLoginRewardDate = Math.trunc(Date.now() / 86400000) * 86400; +}; From ab0d472c755f444dfea249685ec921ea94f3397e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 09:48:45 -0700 Subject: [PATCH 295/776] chore: delete clan invite email when member is kicked before accepting (#1370) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1370 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/removeFromGuildController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 352c58d3..3cf08097 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -1,4 +1,5 @@ import { GuildMember } from "@/src/models/guildModel"; +import { Inbox } from "@/src/models/inboxModel"; import { Account } from "@/src/models/loginModel"; import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; @@ -36,7 +37,12 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { // TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think) } else if (guildMember.status == 2) { - // TODO: Maybe the inbox message for the sent invite should be deleted? + // Delete the inbox message for the invite + await Inbox.deleteOne({ + ownerId: guildMember.accountId, + contextInfo: guild._id.toString(), + acceptAction: "GUILD_INVITE" + }); } await GuildMember.deleteOne({ _id: guildMember._id }); From 8cdcb209aee353f10f467d118d1ccced03bb7068 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:48:53 +0100 Subject: [PATCH 296/776] fix: remove clan key blueprint when removed from guild --- src/controllers/api/removeFromGuildController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 3cf08097..b46e1bb1 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -29,7 +29,7 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { } else { const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); if (recipeIndex != -1) { - inventory.Recipes.splice(itemIndex, 1); + inventory.Recipes.splice(recipeIndex, 1); } } From 69f544c8d18638c8e9e500919b6b4dfc03fa4e50 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 18:01:15 +0100 Subject: [PATCH 297/776] chore: use inventory projection in updateInventoryForConfirmedGuildJoin --- src/services/guildService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 1d5dda15..76a6dae2 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -293,7 +293,7 @@ export const updateInventoryForConfirmedGuildJoin = async ( accountId: string, guildId: Types.ObjectId ): Promise => { - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "GuildId Recipes"); // Set GuildId inventory.GuildId = guildId; From 9de0aee6f0b99b0c602f6f367e75c1978b15c523 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 18:01:52 +0100 Subject: [PATCH 298/776] fix: dojo key blueprint not immediately showing up when creating clan --- src/controllers/api/createGuildController.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index cef7e423..d8757545 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -28,7 +28,17 @@ export const createGuildController: RequestHandler = async (req, res) => { await updateInventoryForConfirmedGuildJoin(accountId, guild._id); - res.json(await getGuildClient(guild, accountId)); + res.json({ + ...(await getGuildClient(guild, accountId)), + InventoryChanges: { + Recipes: [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ] + } + }); }; interface ICreateGuildRequest { From 895b9381ca6fdec13a5952ed369cd5a9246ae543 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:20:54 -0700 Subject: [PATCH 299/776] chore: update eslint (#1373) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1373 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .eslintrc | 6 +- package-lock.json | 308 ++++++++----------- package.json | 10 +- src/controllers/stats/viewController.ts | 2 +- src/models/guildModel.ts | 4 +- src/models/inventoryModels/inventoryModel.ts | 4 +- src/models/inventoryModels/loadoutModel.ts | 4 +- src/models/leaderboardModel.ts | 2 +- src/models/shipModel.ts | 2 +- src/models/statsModel.ts | 2 +- src/services/loginService.ts | 2 +- src/services/saveLoadoutService.ts | 3 +- src/types/personalRoomsTypes.ts | 4 +- src/types/saveLoadoutTypes.ts | 4 +- 14 files changed, 146 insertions(+), 211 deletions(-) diff --git a/.eslintrc b/.eslintrc index 6a62f9a7..e77dc45d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,17 +15,17 @@ "@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/no-unsafe-member-access": "warn", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], "@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-loss-of-precision": "warn", + "no-loss-of-precision": "warn", "@typescript-eslint/no-unnecessary-condition": "warn", + "@typescript-eslint/no-base-to-string": "off", "no-case-declarations": "error", "prettier/prettier": "error", - "@typescript-eslint/semi": "error", "no-mixed-spaces-and-tabs": "error", "require-await": "off", "@typescript-eslint/require-await": "error" diff --git a/package-lock.json b/package-lock.json index 97ecfb8c..c379e26e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,11 +24,11 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.18", - "@typescript-eslint/parser": "^7.18", - "eslint": "^8.56.0", - "eslint-plugin-prettier": "^5.2.3", - "prettier": "^3.4.2", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^8", + "eslint-plugin-prettier": "^5.2.5", + "prettier": "^3.5.3", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0" }, @@ -295,9 +295,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, "license": "MIT", "engines": { @@ -477,80 +477,72 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -558,41 +550,37 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -600,73 +588,85 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "node_modules/@typescript-eslint/utils": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.28.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@ungap/structured-clone": { @@ -794,16 +794,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -1277,19 +1267,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1470,14 +1447,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", + "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.10.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1488,7 +1465,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -2030,27 +2007,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2950,16 +2906,6 @@ "node": ">=16" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3475,16 +3421,6 @@ "is-arrayish": "^0.3.1" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3615,14 +3551,14 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", + "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3744,16 +3680,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-node": { diff --git a/package.json b/package.json index 98e770ec..29ae0aa6 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,11 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.18", - "@typescript-eslint/parser": "^7.18", - "eslint": "^8.56.0", - "eslint-plugin-prettier": "^5.2.3", - "prettier": "^3.4.2", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^8", + "eslint-plugin-prettier": "^5.2.5", + "prettier": "^3.5.3", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0" }, diff --git a/src/controllers/stats/viewController.ts b/src/controllers/stats/viewController.ts index 594efd3b..55fbbb7f 100644 --- a/src/controllers/stats/viewController.ts +++ b/src/controllers/stats/viewController.ts @@ -17,7 +17,7 @@ const viewController: RequestHandler = async (req, res) => { for (const item of inventory.XPInfo) { const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType); if (weaponIndex !== -1) { - responseJson.Weapons[weaponIndex].xp == item.XP; + responseJson.Weapons[weaponIndex].xp = item.XP; } else { responseJson.Weapons.push({ type: item.ItemType, xp: item.XP }); } diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 5ba44fc1..11556893 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -194,12 +194,12 @@ type GuildDocumentProps = { DojoComponents: Types.DocumentArray; }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type type GuildModel = Model; export const Guild = model("Guild", guildSchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TGuildDatabaseDocument = Document & Omit< IGuildDatabase & { diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 794e9d2a..9a3aa949 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1525,12 +1525,12 @@ export type InventoryDocumentProps = { CrewShipSalvagedWeaponsSkins: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type type InventoryModelType = Model; export const Inventory = model("Inventory", inventorySchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TInventoryDatabaseDocument = Document & Omit< IInventoryDatabase & { diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts index dfa90bef..73343c8b 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -95,12 +95,12 @@ type loadoutDocumentProps = { DRIFTER: Types.DocumentArray; }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type type loadoutModelType = Model; export const Loadout = model("Loadout", loadoutSchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TLoadoutDatabaseDocument = Document & Omit< ILoadoutDatabase & { diff --git a/src/models/leaderboardModel.ts b/src/models/leaderboardModel.ts index 0faebddc..2db984d3 100644 --- a/src/models/leaderboardModel.ts +++ b/src/models/leaderboardModel.ts @@ -20,7 +20,7 @@ leaderboardEntrySchema.index({ expiry: 1 }, { expireAfterSeconds: 0 }); // With export const Leaderboard = model("Leaderboard", leaderboardEntrySchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TLeaderboardEntryDocument = Document & { _id: Types.ObjectId; __v: number; diff --git a/src/models/shipModel.ts b/src/models/shipModel.ts index 784a6125..5176defb 100644 --- a/src/models/shipModel.ts +++ b/src/models/shipModel.ts @@ -48,7 +48,7 @@ shipSchema.set("toObject", { export const Ship = model("Ships", shipSchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TShipDatabaseDocument = Document & IShipDatabase & { _id: Types.ObjectId; diff --git a/src/models/statsModel.ts b/src/models/statsModel.ts index 64f31258..62e53c04 100644 --- a/src/models/statsModel.ts +++ b/src/models/statsModel.ts @@ -112,7 +112,7 @@ statsSchema.index({ accountOwnerId: 1 }, { unique: true }); export const Stats = model("Stats", statsSchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TStatsDatabaseDocument = Document & { _id: Types.ObjectId; __v: number; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index df9e6c90..41e33ad3 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -61,7 +61,7 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ await personalRooms.save(); }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TAccountDocument = Document & IDatabaseAccountJson & { _id: Types.ObjectId; __v: number }; diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index d1fc0fcf..038f0d23 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -28,8 +28,7 @@ export const handleInventoryItemConfigChange = async ( ): Promise => { const inventory = await getInventory(accountId); - for (const [_equipmentName, _equipment] of Object.entries(equipmentChanges)) { - const equipment = _equipment as ISaveLoadoutRequestNoUpgradeVer[keyof ISaveLoadoutRequestNoUpgradeVer]; + for (const [_equipmentName, equipment] of Object.entries(equipmentChanges)) { const equipmentName = _equipmentName as keyof ISaveLoadoutRequestNoUpgradeVer; if (isEmptyObject(equipment)) { diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index cfb98ae7..fb672955 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -46,10 +46,10 @@ export type PersonalRoomsDocumentProps = { }; }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type PersonalRoomsModelType = Model; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TPersonalRoomsDatabaseDocument = Document & Omit< IPersonalRoomsDatabase & { diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index 3bef13a8..b496a4ea 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -39,7 +39,7 @@ export interface ISaveLoadoutRequest { WeaponSkins: IItemEntry; } -export interface ISaveLoadoutRequestNoUpgradeVer extends Omit {} +export type ISaveLoadoutRequestNoUpgradeVer = Omit; export interface IOperatorConfigEntry { [configId: string]: IOperatorConfigClient; @@ -53,7 +53,7 @@ export type IConfigEntry = { [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; } & { IsNew?: boolean }; -export interface ILoadoutClient extends Omit {} +export type ILoadoutClient = Omit; // keep in sync with ILoadOutPresets export interface ILoadoutDatabase { From a7899d1c18649d089d2f3894309be2596ad3e9d7 Mon Sep 17 00:00:00 2001 From: Sainan Date: Sat, 29 Mar 2025 15:35:43 -0700 Subject: [PATCH 300/776] feat: give kahl standing when completing the new war (#1334) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1334 --- src/models/inventoryModels/inventoryModel.ts | 16 +++++++++++++++- src/services/questService.ts | 17 +++++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 10 ++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 9a3aa949..8e7d42c1 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -82,7 +82,8 @@ import { INemesisDatabase, INemesisClient, IInfNode, - IDiscoveredMarker + IDiscoveredMarker, + IWeeklyMission } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -445,6 +446,18 @@ kubrowPetEggSchema.set("toJSON", { } }); +const weeklyMissionSchema = new Schema( + { + MissionIndex: Number, + CompletedMission: Boolean, + JobManifest: String, + Challenges: [String], + ChallengesReset: Boolean, + WeekCount: Number + }, + { _id: false } +); + const affiliationsSchema = new Schema( { Initiated: Boolean, @@ -452,6 +465,7 @@ const affiliationsSchema = new Schema( Title: Number, FreeFavorsEarned: { type: [Number], default: undefined }, FreeFavorsUsed: { type: [Number], default: undefined }, + WeeklyMissions: { type: [weeklyMissionSchema], default: undefined }, Tag: String }, { _id: false } diff --git a/src/services/questService.ts b/src/services/questService.ts index a9629339..fccc22fb 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -60,6 +60,23 @@ export const updateQuestKey = async ( inventoryChanges = await addItems(inventory as TInventoryDatabaseDocument, questCompletionItems); } inventory.ActiveQuest = ""; + + if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + inventory.Affiliations.push({ + Title: 1, + Standing: 1, + WeeklyMissions: [ + { + MissionIndex: 0, + CompletedMission: false, + JobManifest: "/Lotus/Syndicates/Kahl/KahlJobManifestVersionThree", + WeekCount: 0, + Challenges: [] + } + ], + Tag: "KahlSyndicate" + }); + } } return inventoryChanges; }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 16e230c8..6f258e13 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -354,9 +354,19 @@ export interface IAffiliation { Title?: number; FreeFavorsEarned?: number[]; FreeFavorsUsed?: number[]; + WeeklyMissions?: IWeeklyMission[]; // Kahl Tag: string; } +export interface IWeeklyMission { + MissionIndex: number; + CompletedMission: boolean; + JobManifest: string; + Challenges: string[]; + ChallengesReset?: boolean; + WeekCount: number; +} + export interface IAlignment { Wisdom: number; Alignment: number; From 1bdc5126b3538ee3394de3727b5fbe5c9caed627 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:42:42 -0700 Subject: [PATCH 301/776] feat: lock worldState time via config (#1361) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1361 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- README.md | 1 + config.json.example | 5 +++-- src/controllers/dynamic/worldStateController.ts | 10 +++++----- src/services/configService.ts | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 55edddd0..ef091058 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,4 @@ To get an idea of what functionality you can expect to be missing [have a look t - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. +- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE. diff --git a/config.json.example b/config.json.example index c8a2df6a..d1c93a66 100644 --- a/config.json.example +++ b/config.json.example @@ -38,10 +38,11 @@ "noDojoResearchTime": false, "fastClanAscension": false, "spoofMasteryRank": -1, - "events": { + "worldState": { "creditBoost": false, "affinityBoost": false, "resourceBoost": false, - "starDays": true + "starDays": true, + "lockTime": 0 } } diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 1de8422d..05c7f60f 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -24,7 +24,7 @@ export const worldStateController: RequestHandler = (req, res) => { typeof req.query.buildLabel == "string" ? req.query.buildLabel.split(" ").join("+") : buildConfig.buildLabel, - Time: Math.round(Date.now() / 1000), + Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), Goals: [], GlobalUpgrades: [], LiteSorties: [], @@ -68,7 +68,7 @@ export const worldStateController: RequestHandler = (req, res) => { ...staticWorldState }; - if (config.events?.starDays) { + if (config.worldState?.starDays) { worldState.Goals.push({ _id: { $oid: "67a4dcce2a198564d62e1647" }, Activation: { $date: { $numberLong: "1738868400000" } }, @@ -117,7 +117,7 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; - if (config.events?.creditBoost) { + if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ _id: { $oid: "5b23106f283a555109666672" }, Activation: { $date: { $numberLong: "1740164400000" } }, @@ -129,7 +129,7 @@ export const worldStateController: RequestHandler = (req, res) => { LocalizeDescTag: "" }); } - if (config.events?.affinityBoost) { + if (config.worldState?.affinityBoost) { worldState.GlobalUpgrades.push({ _id: { $oid: "5b23106f283a555109666673" }, Activation: { $date: { $numberLong: "1740164400000" } }, @@ -141,7 +141,7 @@ export const worldStateController: RequestHandler = (req, res) => { LocalizeDescTag: "" }); } - if (config.events?.resourceBoost) { + if (config.worldState?.resourceBoost) { worldState.GlobalUpgrades.push({ _id: { $oid: "5b23106f283a555109666674" }, Activation: { $date: { $numberLong: "1740164400000" } }, diff --git a/src/services/configService.ts b/src/services/configService.ts index 114eccc9..3aaa2796 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -64,11 +64,12 @@ interface IConfig { noDojoResearchTime?: boolean; fastClanAscension?: boolean; spoofMasteryRank?: number; - events?: { + worldState?: { creditBoost?: boolean; affinityBoost?: boolean; resourceBoost?: boolean; starDays?: boolean; + lockTime?: number; }; } From f7ada5a7e5eaa8a31ab8ca441a46f639219f95c7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 04:40:00 -0700 Subject: [PATCH 302/776] chore: delete guild when founding warlord leaves (#1371) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1371 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/removeFromGuildController.ts | 90 ++++++++++--------- .../custom/deleteAccountController.ts | 9 +- src/services/guildService.ts | 12 +++ 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index b46e1bb1..705d597f 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -1,7 +1,7 @@ import { GuildMember } from "@/src/models/guildModel"; import { Inbox } from "@/src/models/inboxModel"; import { Account } from "@/src/models/loginModel"; -import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; +import { deleteGuild, getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; @@ -18,50 +18,54 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { } const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!; - if (guildMember.status == 0) { - const inventory = await getInventory(payload.userId); - inventory.GuildId = undefined; - - // Remove clan key or blueprint from kicked member - const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); - if (itemIndex != -1) { - inventory.LevelKeys.splice(itemIndex, 1); - } else { - const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); - if (recipeIndex != -1) { - inventory.Recipes.splice(recipeIndex, 1); - } - } - - await inventory.save(); - - // TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think) - } else if (guildMember.status == 2) { - // Delete the inbox message for the invite - await Inbox.deleteOne({ - ownerId: guildMember.accountId, - contextInfo: guild._id.toString(), - acceptAction: "GUILD_INVITE" - }); - } - await GuildMember.deleteOne({ _id: guildMember._id }); - - guild.RosterActivity ??= []; - if (isKick) { - const kickee = (await Account.findById(payload.userId))!; - guild.RosterActivity.push({ - dateTime: new Date(), - entryType: 12, - details: getSuffixedName(kickee) + "," + getSuffixedName(account) - }); + if (guildMember.rank == 0) { + await deleteGuild(guild._id); } else { - guild.RosterActivity.push({ - dateTime: new Date(), - entryType: 7, - details: getSuffixedName(account) - }); + if (guildMember.status == 0) { + const inventory = await getInventory(payload.userId); + inventory.GuildId = undefined; + + // Remove clan key or blueprint from kicked member + const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); + if (itemIndex != -1) { + inventory.LevelKeys.splice(itemIndex, 1); + } else { + const recipeIndex = inventory.Recipes.findIndex( + x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint" + ); + if (recipeIndex != -1) { + inventory.Recipes.splice(recipeIndex, 1); + } + } + + await inventory.save(); + } else if (guildMember.status == 2) { + // Delete the inbox message for the invite + await Inbox.deleteOne({ + ownerId: guildMember.accountId, + contextInfo: guild._id.toString(), + acceptAction: "GUILD_INVITE" + }); + } + await GuildMember.deleteOne({ _id: guildMember._id }); + + guild.RosterActivity ??= []; + if (isKick) { + const kickee = (await Account.findById(payload.userId))!; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 12, + details: getSuffixedName(kickee) + "," + getSuffixedName(account) + }); + } else { + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 7, + details: getSuffixedName(account) + }); + } + await guild.save(); } - await guild.save(); res.json({ _id: payload.userId, diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index e5c466b4..fad4485b 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -9,10 +9,17 @@ import { Ship } from "@/src/models/shipModel"; import { Stats } from "@/src/models/statsModel"; import { GuildMember } from "@/src/models/guildModel"; import { Leaderboard } from "@/src/models/leaderboardModel"; +import { deleteGuild } from "@/src/services/guildService"; export const deleteAccountController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - // TODO: Handle the account being the creator of a guild + + // If account is the founding warlord of a guild, delete that guild as well. + const guildMember = await GuildMember.findOne({ accountId, rank: 0, status: 0 }); + if (guildMember) { + await deleteGuild(guildMember.guildId); + } + await Promise.all([ Account.deleteOne({ _id: accountId }), GuildMember.deleteMany({ accountId: accountId }), diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 76a6dae2..cdaf52fd 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -22,6 +22,7 @@ import { logger } from "../utils/logger"; import { config } from "./configService"; import { Account } from "../models/loginModel"; import { getRandomInt } from "./rngService"; +import { Inbox } from "../models/inboxModel"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -360,3 +361,14 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj } } }; + +export const deleteGuild = async (guildId: Types.ObjectId): Promise => { + await Guild.deleteOne({ _id: guildId }); + await GuildMember.deleteMany({ guildId }); + + // If guild sent any invites, delete those inbox messages as well. + await Inbox.deleteMany({ + contextInfo: guildId.toString(), + acceptAction: "GUILD_INVITE" + }); +}; From c376ff25f3006d2c323223c3d8a3b0a55a44b3ff Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 05:02:42 -0700 Subject: [PATCH 303/776] chore: don't emit code when verifying types in CI (#1380) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1380 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- package.json | 3 ++- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec5e2082..5decfe6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,5 +17,5 @@ jobs: node-version: ${{ matrix.version }} - run: npm ci - run: cp config.json.example config.json - - run: npm run build + - run: npm run verify - run: npm run lint diff --git a/package.json b/package.json index 29ae0aa6..e7cd9652 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", - "build": "tsc && copyfiles static/webui/** build", + "build": "tsc --incremental && copyfiles static/webui/** build", + "verify": "tsc --noEmit", "lint": "eslint --ext .ts .", "lint:fix": "eslint --fix --ext .ts .", "prettier": "prettier --write .", diff --git a/tsconfig.json b/tsconfig.json index fde2ff45..349cffc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, + // "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ From 4cb883dabf8e81c5203288a0023180bf3633a837 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 30 Mar 2025 05:10:24 -0700 Subject: [PATCH 304/776] feat(webui): adding modular K-Drives, Amps and Zaw (#1374) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1374 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../custom/addModularEquipmentController.ts | 21 +++ .../custom/getItemListsController.ts | 8 +- src/routes/custom.ts | 4 +- static/webui/index.html | 36 ++++- static/webui/script.js | 134 +++++++++++++++++- static/webui/translations/de.js | 1 - static/webui/translations/en.js | 1 - static/webui/translations/fr.js | 1 - static/webui/translations/ru.js | 1 - static/webui/translations/zh.js | 1 - 10 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 src/controllers/custom/addModularEquipmentController.ts diff --git a/src/controllers/custom/addModularEquipmentController.ts b/src/controllers/custom/addModularEquipmentController.ts new file mode 100644 index 00000000..5e09902b --- /dev/null +++ b/src/controllers/custom/addModularEquipmentController.ts @@ -0,0 +1,21 @@ +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory, addEquipment, occupySlot, productCategoryToInventoryBin } from "@/src/services/inventoryService"; +import { RequestHandler } from "express"; +import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; + +export const addModularEquipmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IAddModularEquipmentRequest; + const category = modularWeaponTypes[request.ItemType]; + const inventoryBin = productCategoryToInventoryBin(category)!; + const inventory = await getInventory(accountId, `${category} ${inventoryBin}`); + addEquipment(inventory, category, request.ItemType, request.ModularParts); + occupySlot(inventory, inventoryBin, true); + await inventory.save(); + res.end(); +}; + +interface IAddModularEquipmentRequest { + ItemType: string; + ModularParts: string[]; +} diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index accbac1c..196f59ca 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -25,6 +25,7 @@ interface ListedItem { fusionLimit?: number; exalted?: string[]; badReason?: "starter" | "frivolous" | "notraw"; + partType?: string; } const relicQualitySuffixes: Record = { @@ -50,6 +51,7 @@ const getItemListsController: RequestHandler = (req, response) => { res.MechSuits = []; res.miscitems = []; res.Syndicates = []; + res.OperatorAmps = []; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ uniqueName, @@ -79,7 +81,8 @@ const getItemListsController: RequestHandler = (req, response) => { ) { res.ModularParts.push({ uniqueName, - name: getString(item.name, lang) + name: getString(item.name, lang), + partType: item.partType }); if (uniqueName.split("/")[5] != "SentTrainingAmplifier") { res.miscitems.push({ @@ -94,7 +97,8 @@ const getItemListsController: RequestHandler = (req, response) => { item.productCategory == "Melee" || item.productCategory == "SpaceGuns" || item.productCategory == "SpaceMelee" || - item.productCategory == "SentinelWeapons" + item.productCategory == "SentinelWeapons" || + item.productCategory == "OperatorAmps" ) { res[item.productCategory].push({ uniqueName, diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 20f19a59..16359226 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -13,8 +13,9 @@ import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAl import { createAccountController } from "@/src/controllers/custom/createAccountController"; import { createMessageController } from "@/src/controllers/custom/createMessageController"; -import { addCurrencyController } from "../controllers/custom/addCurrencyController"; +import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController"; import { addItemsController } from "@/src/controllers/custom/addItemsController"; +import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController"; import { addXpController } from "@/src/controllers/custom/addXpController"; import { importController } from "@/src/controllers/custom/importController"; @@ -39,6 +40,7 @@ customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); customRouter.post("/addCurrency", addCurrencyController); customRouter.post("/addItems", addItemsController); +customRouter.post("/addModularEquipment", addModularEquipmentController); customRouter.post("/addXp", addXpController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); diff --git a/static/webui/index.html b/static/webui/index.html index f88820e9..dcb2539c 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -193,10 +193,15 @@
- + +
@@ -299,6 +304,15 @@
+
+ + +
+
@@ -309,6 +323,13 @@
+
+ + + + + +
@@ -604,7 +625,6 @@ - @@ -613,6 +633,18 @@ + + + + + + + + + + + + diff --git a/static/webui/script.js b/static/webui/script.js index 8697414e..4e9119fc 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -185,6 +185,16 @@ function fetchItemList() { name: loc("code_traumaticPeculiar") }); + // Add modular weapons + data.OperatorAmps.push({ + uniqueName: "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + name: loc("code_amp") + }); + data.Melee.push({ + uniqueName: "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + name: loc("code_zaw") + }); + const itemMap = { // Generics for rivens "/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": { name: loc("code_archgun") }, @@ -201,14 +211,9 @@ function fetchItemList() { "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": { name: loc("code_kitgun") }, - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": { name: loc("code_zaw") }, "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/OperatorTrainingAmpWeapon": { name: loc("code_moteAmp") }, - "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": { name: loc("code_amp") }, - "/Lotus/Weapons/Operator/Pistols/DrifterPistol/DrifterPistolPlayerWeapon": { - name: loc("code_sirocco") - }, "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kDrive") } }; for (const [type, items] of Object.entries(data)) { @@ -233,6 +238,33 @@ function fetchItemList() { if (type == "Syndicates" && item.uniqueName.startsWith("RadioLegion")) { item.name += " (" + item.uniqueName + ")"; } + if (type == "ModularParts") { + const supportedModularParts = [ + "LWPT_HB_DECK", + "LWPT_HB_ENGINE", + "LWPT_HB_FRONT", + "LWPT_HB_JET", + "LWPT_AMP_OCULUS", + "LWPT_AMP_CORE", + "LWPT_AMP_BRACE", + "LWPT_BLADE", + "LWPT_HILT", + "LWPT_HILT_WEIGHT" + ]; + if (supportedModularParts.includes(item.partType)) { + const option = document.createElement("option"); + option.setAttribute("data-key", item.uniqueName); + option.value = item.name; + document + .getElementById("datalist-" + type + "-" + item.partType.slice(5)) + .appendChild(option); + } else { + const option = document.createElement("option"); + option.setAttribute("data-key", item.uniqueName); + option.value = item.name; + document.getElementById("datalist-" + type).appendChild(option); + } + } if (item.badReason != "notraw") { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); @@ -622,6 +654,67 @@ function doAcquireEquipment(category) { }); } +function doAcquireModularEquipment(category, ItemType) { + let requiredParts; + let ModularParts = []; + switch (category) { + case "HoverBoards": + ItemType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"; + requiredParts = ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"]; + break; + case "OperatorAmps": + requiredParts = ["AMP_OCULUS", "AMP_CORE", "AMP_BRACE"]; + break; + case "Melee": + requiredParts = ["BLADE", "HILT", "HILT_WEIGHT"]; + break; + } + requiredParts.forEach(part => { + const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part)); + if (partName) { + ModularParts.push(partName); + } + }); + if (ModularParts.length != requiredParts.length) { + let isFirstPart = true; + requiredParts.forEach(part => { + const partSelector = document.getElementById("acquire-type-" + category + "-" + part); + if (!getKey(partSelector)) { + if (isFirstPart) { + isFirstPart = false; + $("#acquire-type-" + category + "-" + part) + .addClass("is-invalid") + .focus(); + } else { + $("#acquire-type-" + category + "-" + part).addClass("is-invalid"); + } + } + }); + } else { + revalidateAuthz(() => { + const req = $.post({ + url: "/custom/addModularEquipment?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + ItemType, + ModularParts + }) + }); + req.done(() => { + const mainInput = document.getElementById("acquire-type-" + category); + if (mainInput) { + mainInput.value = ""; + document.getElementById("modular-" + category).style.display = "none"; + } + requiredParts.forEach(part => { + document.getElementById("acquire-type-" + category + "-" + part).value = ""; + }); + updateInventory(); + }); + }); + } +} + $("input[list]").on("input", function () { $(this).removeClass("is-invalid"); }); @@ -1220,3 +1313,34 @@ function toast(text) { toast.appendChild(div); new bootstrap.Toast(document.querySelector(".toast-container").appendChild(toast)).show(); } + +function handleModularSelection(category) { + const modularWeapons = [ + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + ]; + const itemType = getKey(document.getElementById("acquire-type-" + category)); + + if (modularWeapons.includes(itemType)) { + doAcquireModularEquipment(category, itemType); + } else { + doAcquireEquipment(category); + } +} +{ + const modularWeapons = [ + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + ]; + const supportedModularInventoryCategory = ["OperatorAmps", "Melee"]; + supportedModularInventoryCategory.forEach(inventoryCategory => { + document.getElementById("acquire-type-" + inventoryCategory).addEventListener("input", function () { + const modularFields = document.getElementById("modular-" + inventoryCategory); + if (modularWeapons.includes(getKey(this))) { + modularFields.style.display = ""; + } else { + modularFields.style.display = "none"; + } + }); + }); +} diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 4aa6d562..09040ddb 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -15,7 +15,6 @@ dict = { code_zaw: `Zaw`, code_moteAmp: `Anfangsverstärker`, code_amp: `Verstärker`, - code_sirocco: `Sirocco`, code_kDrive: `K-Drive`, code_legendaryCore: `Legendärer Kern`, code_traumaticPeculiar: `Kuriose Mod: Traumatisch`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index ee197c9e..750468db 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -14,7 +14,6 @@ dict = { code_zaw: `Zaw`, code_moteAmp: `Mote Amp`, code_amp: `Amp`, - code_sirocco: `Sirocco`, code_kDrive: `K-Drive`, code_legendaryCore: `Legendary Core`, code_traumaticPeculiar: `Traumatic Peculiar`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index c51695bd..c1d8830d 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -15,7 +15,6 @@ dict = { code_zaw: `Zaw`, code_moteAmp: `Amplificateur Faible`, code_amp: `Amplificateur`, - code_sirocco: `Sirocco`, code_kDrive: `K-Drive`, code_legendaryCore: `Coeur Légendaire`, code_traumaticPeculiar: `Traumatisme Atypique`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 5af34072..7fda4ed1 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -15,7 +15,6 @@ dict = { code_zaw: `Зо`, code_moteAmp: `Пылинка`, code_amp: `Усилитель`, - code_sirocco: `Сирокко`, code_kDrive: `К-Драйв`, code_legendaryCore: `Легендарное ядро`, code_traumaticPeculiar: `Травмирующая Странность`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index dbcf45d0..de64de75 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -15,7 +15,6 @@ dict = { code_zaw: `自制近战`, code_moteAmp: `微尘增幅器`, code_amp: `增幅器`, - code_sirocco: `赤风`, code_kDrive: `K式悬浮板`, code_legendaryCore: `传奇核心`, code_traumaticPeculiar: `创伤怪奇`, From d3819c25c5c33b34147d6557a8cc095f214ae8d8 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 30 Mar 2025 05:10:32 -0700 Subject: [PATCH 305/776] feat(webui): gild action for modular equipment (#1375) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1375 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../custom/gildEquipmentController.ts | 23 +++++++++++ src/routes/custom.ts | 2 + static/webui/script.js | 40 +++++++++++++++++++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 8 files changed, 70 insertions(+) create mode 100644 src/controllers/custom/gildEquipmentController.ts diff --git a/src/controllers/custom/gildEquipmentController.ts b/src/controllers/custom/gildEquipmentController.ts new file mode 100644 index 00000000..46716207 --- /dev/null +++ b/src/controllers/custom/gildEquipmentController.ts @@ -0,0 +1,23 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const gildEquipmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IGildEquipmentRequest; + const inventory = await getInventory(accountId, request.Category); + const weapon = inventory[request.Category].id(request.ItemId); + if (weapon) { + weapon.Features ??= 0; + weapon.Features |= EquipmentFeatures.GILDED; + await inventory.save(); + } + res.end(); +}; + +type IGildEquipmentRequest = { + ItemId: string; + Category: TEquipmentKey; +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 16359226..0834f90b 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -17,6 +17,7 @@ import { addCurrencyController } from "@/src/controllers/custom/addCurrencyContr import { addItemsController } from "@/src/controllers/custom/addItemsController"; import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController"; import { addXpController } from "@/src/controllers/custom/addXpController"; +import { gildEquipmentController } from "@/src/controllers/custom/gildEquipmentController"; import { importController } from "@/src/controllers/custom/importController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; @@ -42,6 +43,7 @@ customRouter.post("/addCurrency", addCurrencyController); customRouter.post("/addItems", addItemsController); customRouter.post("/addModularEquipment", addModularEquipmentController); customRouter.post("/addXp", addXpController); +customRouter.post("/gildEquipment", gildEquipmentController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); diff --git a/static/webui/script.js b/static/webui/script.js index 4e9119fc..f7abb431 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -287,6 +287,20 @@ function updateInventory() { window.itemListPromise.then(itemMap => { window.didInitialInventoryUpdate = true; + const modularWeapons = [ + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit" + ]; + // Populate inventory route ["RegularCredits", "PremiumCredits", "FusionPoints", "PrimeTokens"].forEach(currency => { document.getElementById(currency + "-owned").textContent = loc("currency_owned") @@ -373,6 +387,17 @@ function updateInventory() { a.innerHTML = ``; td.appendChild(a); } + if (!(item.Features & 8) && modularWeapons.includes(item.ItemType)) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + gildEquipment(category, item.ItemId.$oid); + }; + a.title = loc("code_gild"); + a.innerHTML = ``; + td.appendChild(a); + } if (category == "Suits") { const a = document.createElement("a"); a.href = "/webui/powersuit/" + item.ItemId.$oid; @@ -899,6 +924,21 @@ function disposeOfItems(category, type, count) { }); } +function gildEquipment(category, oid) { + revalidateAuthz(() => { + $.post({ + url: "/custom/gildEquipment?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + ItemId: oid, + Category: category + }) + }).done(function () { + updateInventory(); + }); + }); +} + function doAcquireMiscItems() { const uniqueName = getKey(document.getElementById("miscitem-type")); if (!uniqueName) { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 09040ddb..cac0623f 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -39,6 +39,7 @@ dict = { code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`, code_succImport: `Erfolgreich importiert.`, + code_gild: `[UNTRANSLATED] Gild`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 750468db..251c9ccc 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -38,6 +38,7 @@ dict = { code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`, code_addModsConfirm: `Are you sure you want to add |COUNT| mods to your account?`, code_succImport: `Successfully imported.`, + code_gild: `Gild`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index c1d8830d..b9bff424 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -39,6 +39,7 @@ dict = { code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`, code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, code_succImport: `Importé.`, + code_gild: `[UNTRANSLATED] Gild`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 7fda4ed1..1efbc311 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -39,6 +39,7 @@ dict = { code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`, code_addModsConfirm: `Вы уверены, что хотите добавить |COUNT| модов на ваш аккаунт?`, code_succImport: `Успешно импортировано.`, + code_gild: `Улучшить`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index de64de75..cf23e042 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -39,6 +39,7 @@ dict = { code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, code_succImport: `导入成功。`, + code_gild: `[UNTRANSLATED] Gild`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, From fa34b9997624435251eb6b154a7545646bf96338 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 05:11:17 -0700 Subject: [PATCH 306/776] chore: improve login error for unknown email + no auto-create (#1379) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1379 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 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index af7beb55..f01c405a 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -54,8 +54,12 @@ export const loginController: RequestHandler = async (request, response) => { } } - //email not found or incorrect password - if (!account || !isCorrectPassword(loginRequest.password, account.password)) { + if (!account) { + response.status(400).json({ error: "unknown user" }); + return; + } + + if (!isCorrectPassword(loginRequest.password, account.password)) { response.status(400).json({ error: "incorrect login data" }); return; } From 725efcc72e34fd1da4051eb024c0bd6f52abe7ac Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 06:20:53 -0700 Subject: [PATCH 307/776] chore: replace copyfiles with ncp (#1381) They're both unmaintained, but this one is smaller at least. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1381 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 287 ++++------------------------------------------ package.json | 4 +- 2 files changed, 26 insertions(+), 265 deletions(-) diff --git a/package-lock.json b/package-lock.json index c379e26e..00f60a01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,12 @@ "dependencies": { "@types/express": "^5", "@types/morgan": "^1.9.9", - "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", "json-with-bigint": "^3.2.2", "mongoose": "^8.11.0", "morgan": "^1.10.0", + "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", "warframe-public-export-plus": "^0.5.48", "warframe-riven-info": "^0.1.2", @@ -746,6 +746,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -755,6 +756,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -804,6 +806,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/basic-auth": { @@ -1014,17 +1017,6 @@ "node": ">= 6" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -1039,6 +1031,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1092,6 +1085,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -1133,53 +1127,6 @@ "node": ">=6.6.0" } }, - "node_modules/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "license": "MIT", - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "node_modules/copyfiles/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/copyfiles/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -1310,12 +1257,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -1361,15 +1302,6 @@ "node": ">= 0.4" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1863,6 +1795,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -1889,15 +1822,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1940,6 +1864,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -1973,6 +1898,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1983,6 +1909,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2130,6 +2057,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -2196,15 +2124,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2256,12 +2175,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2528,6 +2441,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -2700,6 +2614,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2709,16 +2632,6 @@ "node": ">= 0.6" } }, - "node_modules/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "license": "ISC", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2875,6 +2788,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2958,12 +2872,6 @@ "node": ">=6.0.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3058,18 +2966,6 @@ "node": ">=0.10.0" } }, - "node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3083,15 +2979,6 @@ "node": ">=8.10.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -3469,30 +3356,11 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3580,52 +3448,6 @@ "dev": true, "license": "MIT" }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3898,15 +3720,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4106,23 +3919,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4133,47 +3929,12 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index e7cd9652..15c4cbfe 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", - "build": "tsc --incremental && copyfiles static/webui/** build", + "build": "tsc --incremental && ncp static/webui build/static/webui", "verify": "tsc --noEmit", "lint": "eslint --ext .ts .", "lint:fix": "eslint --fix --ext .ts .", @@ -17,12 +17,12 @@ "dependencies": { "@types/express": "^5", "@types/morgan": "^1.9.9", - "copyfiles": "^2.4.1", "crc-32": "^1.2.2", "express": "^5", "json-with-bigint": "^3.2.2", "mongoose": "^8.11.0", "morgan": "^1.10.0", + "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", "warframe-public-export-plus": "^0.5.48", "warframe-riven-info": "^0.1.2", From c82cad7b027bc089d4f8d7a650883331509babad Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 30 Mar 2025 06:39:32 -0700 Subject: [PATCH 308/776] chore(webui): update ru loc (#1384) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1384 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 1efbc311..3fd2575f 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -28,7 +28,7 @@ dict = { code_succRankUp: `Ранг успешно повышен`, code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`, code_succAdded: `Успешно добавлено.`, - code_succRemoved: `[UNTRANSLATED] Successfully removed.`, + code_succRemoved: `Успешно удалено.`, code_buffsNumber: `Количество усилений`, code_cursesNumber: `Количество проклятий`, code_rerollsNumber: `Количество циклов`, From f34e1615e2cb8dc58faa03ec8400613038823707 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 30 Mar 2025 06:54:58 -0700 Subject: [PATCH 309/776] fix(webui): show 0 rerolls instead NaN in Rivens (#1385) Co-authored-by: Sainan Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1385 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index f7abb431..05bb8600 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -465,7 +465,7 @@ function updateInventory() { " ⟳ " + - parseInt(fingerprint.rerolls) + + (fingerprint.rerolls ?? 0) + ""; tr.appendChild(td); } From b07e89ed722587266b348a5e70f9d1acb9aa2415 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sun, 30 Mar 2025 07:13:48 -0700 Subject: [PATCH 310/776] chore(webui): update to German translation (#1386) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1386 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index cac0623f..89684cea 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -39,7 +39,7 @@ dict = { code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`, code_succImport: `Erfolgreich importiert.`, - code_gild: `[UNTRANSLATED] Gild`, + code_gild: `Veredeln`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, From b6167165fef47f1812ec2c70d1422803462b00dd Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 30 Mar 2025 08:12:31 -0700 Subject: [PATCH 311/776] chore(webui): put all ModularParts in itemLists (#1383) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1383 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 196f59ca..5b3ee13b 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -68,17 +68,7 @@ const getItemListsController: RequestHandler = (req, response) => { } } for (const [uniqueName, item] of Object.entries(ExportWeapons)) { - if ( - uniqueName.split("/")[4] == "OperatorAmplifiers" || - uniqueName.split("/")[5] == "SUModularSecondarySet1" || - uniqueName.split("/")[5] == "SUModularPrimarySet1" || - uniqueName.split("/")[5] == "InfKitGun" || - uniqueName.split("/")[5] == "HoverboardParts" || - uniqueName.split("/")[5] == "ModularMelee01" || - uniqueName.split("/")[5] == "ModularMelee02" || - uniqueName.split("/")[5] == "ModularMeleeInfested" || - uniqueName.split("/")[6] == "CreaturePetParts" - ) { + if (item.partType) { res.ModularParts.push({ uniqueName, name: getString(item.name, lang), From 779bc340823f5393084fe056a104ab1e6e29d0d1 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 30 Mar 2025 08:12:46 -0700 Subject: [PATCH 312/776] feat(webui): adding kitgun (#1382) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1382 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../custom/addModularEquipmentController.ts | 20 +++++++++++- static/webui/index.html | 18 +++++++++-- static/webui/script.js | 32 +++++++++++++++---- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/controllers/custom/addModularEquipmentController.ts b/src/controllers/custom/addModularEquipmentController.ts index 5e09902b..ea51e2d3 100644 --- a/src/controllers/custom/addModularEquipmentController.ts +++ b/src/controllers/custom/addModularEquipmentController.ts @@ -1,7 +1,8 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory, addEquipment, occupySlot, productCategoryToInventoryBin } from "@/src/services/inventoryService"; -import { RequestHandler } from "express"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { ExportWeapons } from "warframe-public-export-plus"; +import { RequestHandler } from "express"; export const addModularEquipmentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -9,6 +10,23 @@ export const addModularEquipmentController: RequestHandler = async (req, res) => const category = modularWeaponTypes[request.ItemType]; const inventoryBin = productCategoryToInventoryBin(category)!; const inventory = await getInventory(accountId, `${category} ${inventoryBin}`); + request.ModularParts.forEach(part => { + if (ExportWeapons[part].gunType) { + if (category == "LongGuns") { + request.ItemType = { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" + }[ExportWeapons[part].gunType]; + } else { + request.ItemType = { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }[ExportWeapons[part].gunType]; + } + } + }); addEquipment(inventory, category, request.ItemType, request.ModularParts); occupySlot(inventory, inventoryBin, true); await inventory.save(); diff --git a/static/webui/index.html b/static/webui/index.html index dcb2539c..f5df02df 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -163,10 +163,15 @@
-
+
+
@@ -179,10 +184,15 @@
-
+
+
@@ -645,6 +655,10 @@ + + + + diff --git a/static/webui/script.js b/static/webui/script.js index 05bb8600..73783972 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -194,6 +194,14 @@ function fetchItemList() { uniqueName: "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", name: loc("code_zaw") }); + data.LongGuns.push({ + uniqueName: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + name: loc("code_kitgun") + }); + data.Pistols.push({ + uniqueName: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + name: loc("code_kitgun") + }); const itemMap = { // Generics for rivens @@ -203,12 +211,10 @@ function fetchItemList() { "/Lotus/Weapons/Tenno/Rifle/LotusRifle": { name: loc("code_rifle") }, "/Lotus/Weapons/Tenno/Shotgun/LotusShotgun": { name: loc("code_shotgun") }, // Modular weapons - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": { name: loc("code_kitgun") }, - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": { name: loc("code_kitgun") }, "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/OperatorTrainingAmpWeapon": { @@ -249,7 +255,11 @@ function fetchItemList() { "LWPT_AMP_BRACE", "LWPT_BLADE", "LWPT_HILT", - "LWPT_HILT_WEIGHT" + "LWPT_HILT_WEIGHT", + "LWPT_GUN_PRIMARY_HANDLE", + "LWPT_GUN_SECONDARY_HANDLE", + "LWPT_GUN_BARREL", + "LWPT_GUN_CLIP" ]; if (supportedModularParts.includes(item.partType)) { const option = document.createElement("option"); @@ -693,6 +703,12 @@ function doAcquireModularEquipment(category, ItemType) { case "Melee": requiredParts = ["BLADE", "HILT", "HILT_WEIGHT"]; break; + case "LongGuns": + requiredParts = ["GUN_BARREL", "GUN_PRIMARY_HANDLE", "GUN_CLIP"]; + break; + case "Pistols": + requiredParts = ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"]; + break; } requiredParts.forEach(part => { const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part)); @@ -1357,7 +1373,9 @@ function toast(text) { function handleModularSelection(category) { const modularWeapons = [ "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary" ]; const itemType = getKey(document.getElementById("acquire-type-" + category)); @@ -1370,9 +1388,11 @@ function handleModularSelection(category) { { const modularWeapons = [ "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary" ]; - const supportedModularInventoryCategory = ["OperatorAmps", "Melee"]; + const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols"]; supportedModularInventoryCategory.forEach(inventoryCategory => { document.getElementById("acquire-type-" + inventoryCategory).addEventListener("input", function () { const modularFields = document.getElementById("modular-" + inventoryCategory); From 3beb1ecc42f2f5ed2d0d3905981fe98a66cb5f8c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 08:13:11 -0700 Subject: [PATCH 313/776] chore: use ExportKeys for quests not in questCompletionItems (#1377) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1377 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/itemDataService.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 9be6c180..27d23616 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -203,12 +203,27 @@ export const getNode = (nodeName: string): IRegion => { }; export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { - const items = (questCompletionItems as unknown as Record | undefined)?.[questKey]; + if (questKey in questCompletionItems) { + return questCompletionItems[questKey as keyof typeof questCompletionItems]; + } + logger.warn(`Quest ${questKey} not found in questCompletionItems`); - if (!items) { - logger.error( - `Quest ${questKey} not found in questCompletionItems, quest completion items have not been given. This is a temporary solution` - ); + const items: ITypeCount[] = []; + const meta = ExportKeys[questKey]; + if (meta.rewards) { + for (const reward of meta.rewards) { + if (reward.rewardType == "RT_STORE_ITEM") { + items.push({ + ItemType: fromStoreItem(reward.itemType), + ItemCount: 1 + }); + } else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") { + items.push({ + ItemType: reward.itemType, + ItemCount: reward.amount + }); + } + } } return items; }; From cfc15246195ff714a8353aa088e4bbcb290abd1d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 08:13:24 -0700 Subject: [PATCH 314/776] fix: give quest completion items from cheated completion too (#1376) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1376 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 17 +++++++++++++++++ src/services/questService.ts | 29 +++++++++++++---------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9bc65bc9..43bb3e8a 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1338,3 +1338,20 @@ const createCalendar = (): ICalendarProgress => { } }; }; + +export const setupKahlSyndicate = (inventory: TInventoryDatabaseDocument): void => { + inventory.Affiliations.push({ + Title: 1, + Standing: 1, + WeeklyMissions: [ + { + MissionIndex: 0, + CompletedMission: false, + JobManifest: "/Lotus/Syndicates/Kahl/KahlJobManifestVersionThree", + WeekCount: 0, + Challenges: [] + } + ], + Tag: "KahlSyndicate" + }); +}; diff --git a/src/services/questService.ts b/src/services/questService.ts index fccc22fb..2e32ea1d 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -2,7 +2,7 @@ import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredIte import { isEmptyObject } from "@/src/helpers/general"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; -import { addItem, addItems, addKeyChainItems } from "@/src/services/inventoryService"; +import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "@/src/services/inventoryService"; import { fromStoreItem, getKeyChainMessage, @@ -62,20 +62,7 @@ export const updateQuestKey = async ( inventory.ActiveQuest = ""; if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { - inventory.Affiliations.push({ - Title: 1, - Standing: 1, - WeeklyMissions: [ - { - MissionIndex: 0, - CompletedMission: false, - JobManifest: "/Lotus/Syndicates/Kahl/KahlJobManifestVersionThree", - WeekCount: 0, - Challenges: [] - } - ], - Tag: "KahlSyndicate" - }); + setupKahlSyndicate(inventory as TInventoryDatabaseDocument); } } return inventoryChanges; @@ -211,8 +198,18 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest } } } + + const questCompletionItems = getQuestCompletionItems(questKey); + logger.debug(`quest completion items`, questCompletionItems); + if (questCompletionItems) { + await addItems(inventory, questCompletionItems); + } + inventory.ActiveQuest = ""; - //TODO: handle quest completion items + + if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + setupKahlSyndicate(inventory); + } }; export const giveKeyChainItem = async ( From fccdbf4a8e4998e7ef98a5c448ec16b464f3a940 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 09:58:44 -0700 Subject: [PATCH 315/776] fix: detect kuva weapons more reliably (#1388) it seems not all of them have the InnateDamageRandomMod or even VT_KUVA so just assuming that any weapon with max rank 40 that's not the ballas sword needs it Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 43bb3e8a..92543e6f 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -377,10 +377,7 @@ export const addItem = async ( if (premiumPurchase) { defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; } - if ( - weapon.defaultUpgrades?.[0]?.ItemType == - "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod" - ) { + if (weapon.maxLevelCap == 40 && typeName.indexOf("BallasSword") == -1) { defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod"; defaultOverwrites.UpgradeFingerprint = JSON.stringify({ compat: typeName, From 516f822e43c81909add625c40c2648909d9fddb8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 09:58:51 -0700 Subject: [PATCH 316/776] feat: clan tiers (#1378) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1378 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/abortDojoComponentController.ts | 2 +- .../contributeToDojoComponentController.ts | 21 ++- .../api/contributeToVaultController.ts | 14 +- .../api/dojoComponentRushController.ts | 15 +- src/controllers/api/guildTechController.ts | 69 ++----- src/services/guildService.ts | 174 ++++++++++++++++-- 6 files changed, 205 insertions(+), 90 deletions(-) diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts index 0ad1f074..3fcf770c 100644 --- a/src/controllers/api/abortDojoComponentController.ts +++ b/src/controllers/api/abortDojoComponentController.ts @@ -32,7 +32,7 @@ export const abortDojoComponentController: RequestHandler = async (req, res) => if (request.DecoId) { removeDojoDeco(guild, request.ComponentId, request.DecoId); } else { - removeDojoRoom(guild, request.ComponentId); + await removeDojoRoom(guild, request.ComponentId); } await guild.save(); diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 05859ad4..3177d5b6 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -94,10 +94,10 @@ const processContribution = ( component.RegularCredits += request.VaultCredits; guild.VaultRegularCredits! -= request.VaultCredits; } - if (component.RegularCredits > scaleRequiredCount(meta.price)) { + if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) { guild.VaultRegularCredits ??= 0; - guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price); - component.RegularCredits = scaleRequiredCount(meta.price); + guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price); + component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); } component.MiscItems ??= []; @@ -108,10 +108,10 @@ const processContribution = ( const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; if ( componentMiscItem.ItemCount + ingredientContribution.ItemCount > - scaleRequiredCount(ingredientMeta.ItemCount) + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) ) { ingredientContribution.ItemCount = - scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; } componentMiscItem.ItemCount += ingredientContribution.ItemCount; } else { @@ -129,10 +129,10 @@ const processContribution = ( const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; if ( componentMiscItem.ItemCount + ingredientContribution.ItemCount > - scaleRequiredCount(ingredientMeta.ItemCount) + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) ) { ingredientContribution.ItemCount = - scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; } componentMiscItem.ItemCount += ingredientContribution.ItemCount; } else { @@ -150,11 +150,14 @@ const processContribution = ( inventoryChanges.MiscItems = miscItemChanges; } - if (component.RegularCredits >= scaleRequiredCount(meta.price)) { + if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { let fullyFunded = true; for (const ingredient of meta.ingredients) { const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); - if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) { + if ( + !componentMiscItem || + componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount) + ) { fullyFunded = false; break; } diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index b1ac10d8..58079e02 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,5 +1,5 @@ import { GuildMember } from "@/src/models/guildModel"; -import { getGuildForRequestEx } from "@/src/services/guildService"; +import { addVaultMiscItems, getGuildForRequestEx } from "@/src/services/guildService"; import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -23,11 +23,17 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { guildMember.RegularCreditsContributed += request.RegularCredits; } if (request.MiscItems.length) { - guild.VaultMiscItems ??= []; + addVaultMiscItems(guild, request.MiscItems); + guildMember.MiscItemsContributed ??= []; for (const item of request.MiscItems) { - guild.VaultMiscItems.push(item); - guildMember.MiscItemsContributed.push(item); + const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); + if (miscItemContribution) { + miscItemContribution.ItemCount += item.ItemCount; + } else { + guildMember.MiscItemsContributed.push(item); + } + addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 5f334356..33aec126 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -1,4 +1,4 @@ -import { GuildMember } from "@/src/models/guildModel"; +import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; @@ -36,10 +36,10 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { if (request.DecoId) { const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; - processContribution(deco, meta, platinumDonated); + processContribution(guild, deco, meta, platinumDonated); } else { const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; - processContribution(component, meta, platinumDonated); + processContribution(guild, component, meta, platinumDonated); const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); if (entry) { @@ -61,8 +61,13 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { }); }; -const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => { - const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice); +const processContribution = ( + guild: TGuildDatabaseDocument, + component: IDojoContributable, + meta: IDojoBuild, + platinumDonated: number +): void => { + const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice); const fullDurationSeconds = meta.time; const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; component.CompletionTime = new Date( diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 58c505b0..35b4d626 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -4,10 +4,13 @@ import { getGuildVault, hasAccessToDojo, hasGuildPermission, + processFundedGuildTechProject, + processGuildTechProjectContributionsUpdate, removePigmentsFromGuildMembers, - scaleRequiredCount + scaleRequiredCount, + setGuildTechLogState } from "@/src/services/guildService"; -import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addItem, @@ -20,8 +23,8 @@ import { import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; -import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; -import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; +import { GuildMember } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; @@ -44,7 +47,7 @@ export const guildTechController: RequestHandler = async (req, res) => { if (project.CompletionDate) { techProject.CompletionDate = toMongoDate(project.CompletionDate); if (Date.now() >= project.CompletionDate.getTime()) { - needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate); + needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate); } } techProjects.push(techProject); @@ -66,17 +69,17 @@ export const guildTechController: RequestHandler = async (req, res) => { guild.TechProjects[ guild.TechProjects.push({ ItemType: data.RecipeType, - ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price), + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), ReqItems: recipe.ingredients.map(x => ({ ItemType: x.ItemType, - ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount) + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) })), State: 0 }) - 1 ]; - setTechLogState(guild, techProject.ItemType, 5); + setGuildTechLogState(guild, techProject.ItemType, 5); if (config.noDojoResearchCosts) { - processFundedProject(guild, techProject, recipe); + processFundedGuildTechProject(guild, techProject, recipe); } else { if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { guild.ActiveDojoColorResearch = data.RecipeType; @@ -151,15 +154,8 @@ export const guildTechController: RequestHandler = async (req, res) => { const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); inventoryChanges.MiscItems = miscItemChanges; - if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { - // This research is now fully funded. - const recipe = ExportDojoRecipes.research[data.RecipeType]; - processFundedProject(guild, techProject, recipe); - if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { - guild.ActiveDojoColorResearch = ""; - await removePigmentsFromGuildMembers(guild._id); - } - } + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); await guild.save(); await inventory.save(); @@ -238,43 +234,6 @@ export const guildTechController: RequestHandler = async (req, res) => { } }; -const processFundedProject = ( - guild: TGuildDatabaseDocument, - techProject: ITechProjectDatabase, - recipe: IDojoResearch -): void => { - techProject.State = 1; - techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); - if (recipe.guildXpValue) { - guild.XP += recipe.guildXpValue; - } - setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); -}; - -const setTechLogState = ( - guild: TGuildDatabaseDocument, - type: string, - state: number, - dateTime: Date | undefined = undefined -): boolean => { - guild.TechChanges ??= []; - const entry = guild.TechChanges.find(x => x.details == type); - if (entry) { - if (entry.entryType == state) { - return false; - } - entry.dateTime = dateTime; - entry.entryType = state; - } else { - guild.TechChanges.push({ - dateTime: dateTime, - entryType: state, - details: type - }); - } - return true; -}; - type TGuildTechRequest = | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } | IGuildTechBasicRequest diff --git a/src/services/guildService.ts b/src/services/guildService.ts index cdaf52fd..19bbf803 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -13,16 +13,18 @@ import { IGuildClient, IGuildMemberClient, IGuildMemberDatabase, - IGuildVault + IGuildVault, + ITechProjectDatabase } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; -import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; +import { ExportDojoRecipes, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; import { config } from "./configService"; import { Account } from "../models/loginModel"; import { getRandomInt } from "./rngService"; import { Inbox } from "../models/inboxModel"; +import { ITypeCount } from "../types/inventoryTypes/inventoryTypes"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -152,6 +154,27 @@ export const getDojoClient = async ( entry.entryType = 1; needSave = true; } + + let newTier: number | undefined; + switch (dojoComponent.pf) { + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksShadow.level": + newTier = 2; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksStorm.level": + newTier = 3; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMountain.level": + newTier = 4; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMoon.level": + newTier = 5; + break; + } + if (newTier) { + logger.debug(`clan finished building barracks, updating to tier ${newTier}`); + await setGuildTier(guild, newTier); + needSave = true; + } } if (dojoComponent.DestructionTime) { if (Date.now() >= dojoComponent.DestructionTime.getTime()) { @@ -189,22 +212,26 @@ export const getDojoClient = async ( if (roomsToRemove.length) { logger.debug(`removing now-destroyed rooms`, roomsToRemove); for (const id of roomsToRemove) { - removeDojoRoom(guild, id); + await removeDojoRoom(guild, id); } needSave = true; } if (needSave) { await guild.save(); } + dojo.Tier = guild.Tier; return dojo; }; -export const scaleRequiredCount = (count: number): number => { - // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. - return Math.max(1, Math.trunc(count / 100)); +const guildTierScalingFactors = [0.01, 0.03, 0.1, 0.3, 1]; +export const scaleRequiredCount = (tier: number, count: number): number => { + return Math.max(1, Math.trunc(count * guildTierScalingFactors[tier - 1])); }; -export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types.ObjectId | string): void => { +export const removeDojoRoom = async ( + guild: TGuildDatabaseDocument, + componentId: Types.ObjectId | string +): Promise => { const component = guild.DojoComponents.splice( guild.DojoComponents.findIndex(x => x._id.equals(componentId)), 1 @@ -223,6 +250,21 @@ export const removeDojoRoom = (guild: TGuildDatabaseDocument, componentId: Types guild.RoomChanges.splice(index, 1); } } + + switch (component.pf) { + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksShadow.level": + await setGuildTier(guild, 1); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksStorm.level": + await setGuildTier(guild, 2); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMountain.level": + await setGuildTier(guild, 3); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMoon.level": + await setGuildTier(guild, 4); + break; + } }; export const removeDojoDeco = ( @@ -248,15 +290,7 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon guild.VaultRegularCredits += component.RegularCredits; } if (component.MiscItems) { - guild.VaultMiscItems ??= []; - for (const componentMiscItem of component.MiscItems) { - const vaultMiscItem = guild.VaultMiscItems.find(x => x.ItemType == componentMiscItem.ItemType); - if (vaultMiscItem) { - vaultMiscItem.ItemCount += componentMiscItem.ItemCount; - } else { - guild.VaultMiscItems.push(componentMiscItem); - } - } + addVaultMiscItems(guild, component.MiscItems); } if (component.RushPlatinum) { guild.VaultPremiumCredits ??= 0; @@ -264,6 +298,18 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon } }; +export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => { + guild.VaultMiscItems ??= []; + for (const miscItem of miscItems) { + const vaultMiscItem = guild.VaultMiscItems.find(x => x.ItemType == miscItem.ItemType); + if (vaultMiscItem) { + vaultMiscItem.ItemCount += miscItem.ItemCount; + } else { + guild.VaultMiscItems.push(miscItem); + } + } +}; + export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { if (build.guildXpValue) { guild.ClaimedXP ??= []; @@ -362,6 +408,102 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj } }; +export const processGuildTechProjectContributionsUpdate = async ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase +): Promise => { + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + // This research is now fully funded. + + if ( + techProject.State == 0 && + techProject.ItemType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/" + ) { + guild.ActiveDojoColorResearch = ""; + await removePigmentsFromGuildMembers(guild._id); + } + + const recipe = ExportDojoRecipes.research[techProject.ItemType]; + processFundedGuildTechProject(guild, techProject, recipe); + } +}; + +export const processFundedGuildTechProject = ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase, + recipe: IDojoResearch +): void => { + techProject.State = 1; + techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); + if (recipe.guildXpValue) { + guild.XP += recipe.guildXpValue; + } + setGuildTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); +}; + +export const setGuildTechLogState = ( + guild: TGuildDatabaseDocument, + type: string, + state: number, + dateTime: Date | undefined = undefined +): boolean => { + guild.TechChanges ??= []; + const entry = guild.TechChanges.find(x => x.details == type); + if (entry) { + if (entry.entryType == state) { + return false; + } + entry.dateTime = dateTime; + entry.entryType = state; + } else { + guild.TechChanges.push({ + dateTime: dateTime, + entryType: state, + details: type + }); + } + return true; +}; + +const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Promise => { + const oldTier = guild.Tier; + guild.Tier = newTier; + if (guild.TechProjects) { + for (const project of guild.TechProjects) { + if (project.State == 1) { + continue; + } + + const meta = ExportDojoRecipes.research[project.ItemType]; + + { + const numContributed = scaleRequiredCount(oldTier, meta.price) - project.ReqCredits; + project.ReqCredits = scaleRequiredCount(newTier, meta.price) - numContributed; + if (project.ReqCredits < 0) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += project.ReqCredits * -1; + project.ReqCredits = 0; + } + } + + for (let i = 0; i != project.ReqItems.length; ++i) { + const numContributed = + scaleRequiredCount(oldTier, meta.ingredients[i].ItemCount) - project.ReqItems[i].ItemCount; + project.ReqItems[i].ItemCount = + scaleRequiredCount(newTier, meta.ingredients[i].ItemCount) - numContributed; + if (project.ReqItems[i].ItemCount < 0) { + project.ReqItems[i].ItemCount *= -1; + addVaultMiscItems(guild, [project.ReqItems[i]]); + project.ReqItems[i].ItemCount = 0; + } + } + + // Check if research is fully funded now due to lowered requirements. + await processGuildTechProjectContributionsUpdate(guild, project); + } + } +}; + export const deleteGuild = async (guildId: Types.ObjectId): Promise => { await Guild.deleteOne({ _id: guildId }); await GuildMember.deleteMany({ guildId }); From 9e99d0370c4c58965de42661727287d96e495dde Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 13:50:36 -0700 Subject: [PATCH 317/776] fix: align dojo component DestructionTime to full seconds (#1394) not doing this causes the client to spam requests and have some UI bugs Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1394 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/queueDojoComponentDestructionController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/queueDojoComponentDestructionController.ts b/src/controllers/api/queueDojoComponentDestructionController.ts index 361f91f8..037b5c27 100644 --- a/src/controllers/api/queueDojoComponentDestructionController.ts +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -16,7 +16,7 @@ export const queueDojoComponentDestructionController: RequestHandler = async (re const componentId = req.query.componentId as string; guild.DojoComponents.id(componentId)!.DestructionTime = new Date( - Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000) + (Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000 ); await guild.save(); From 01f04c287a61914dabf11e349c71a82dd2aa7e49 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 30 Mar 2025 13:50:59 -0700 Subject: [PATCH 318/776] fix: add RemovedIdItems to valence fusion response (#1397) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1397 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/nemesisController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index b1f129db..cf15ef37 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -18,7 +18,7 @@ export const nemesisController: RequestHandler = async (req, res) => { const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; - // Upgrade destination damage type if desireed + // Update destination damage type if desired if (body.UseSourceDmgType) { destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag; } @@ -42,7 +42,8 @@ export const nemesisController: RequestHandler = async (req, res) => { await inventory.save(); res.json({ InventoryChanges: { - [body.Category]: [destWeapon.toJSON()] + [body.Category]: [destWeapon.toJSON()], + RemovedIdItems: [{ ItemId: body.SourceWeapon }] } }); } else if ((req.query.mode as string) == "p") { From 48598c145f24aee2c4e570076da1a48949c06c3d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:14:00 -0700 Subject: [PATCH 319/776] feat: guild ads (#1390) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1390 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/cancelGuildAdvertisementController.ts | 20 +++++ .../contributeToDojoComponentController.ts | 4 +- .../api/contributeToVaultController.ts | 14 ++-- src/controllers/api/guildTechController.ts | 4 +- .../api/postGuildAdvertisementController.ts | 75 +++++++++++++++++++ .../dynamic/getGuildAdsController.ts | 26 +++++++ src/models/guildModel.ts | 20 ++++- src/routes/api.ts | 4 + src/routes/dynamic.ts | 2 + src/services/guildService.ts | 18 ++++- src/types/guildTypes.ts | 25 +++++++ 11 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 src/controllers/api/cancelGuildAdvertisementController.ts create mode 100644 src/controllers/api/postGuildAdvertisementController.ts create mode 100644 src/controllers/dynamic/getGuildAdsController.ts diff --git a/src/controllers/api/cancelGuildAdvertisementController.ts b/src/controllers/api/cancelGuildAdvertisementController.ts new file mode 100644 index 00000000..aa587201 --- /dev/null +++ b/src/controllers/api/cancelGuildAdvertisementController.ts @@ -0,0 +1,20 @@ +import { GuildAd } from "@/src/models/guildModel"; +import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = await getGuildForRequestEx(req, inventory); + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) { + res.status(400).end(); + return; + } + + await GuildAd.deleteOne({ GuildId: guild._id }); + + res.end(); +}; diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 3177d5b6..acdbf8a4 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -1,6 +1,7 @@ import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { + addGuildMemberMiscItemContribution, getDojoClient, getGuildForRequestEx, hasAccessToDojo, @@ -143,8 +144,7 @@ const processContribution = ( ItemCount: ingredientContribution.ItemCount * -1 }); - guildMember.MiscItemsContributed ??= []; - guildMember.MiscItemsContributed.push(ingredientContribution); + addGuildMemberMiscItemContribution(guildMember, ingredientContribution); } addMiscItems(inventory, miscItemChanges); inventoryChanges.MiscItems = miscItemChanges; diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index 58079e02..74dc91a4 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,5 +1,9 @@ import { GuildMember } from "@/src/models/guildModel"; -import { addVaultMiscItems, getGuildForRequestEx } from "@/src/services/guildService"; +import { + addGuildMemberMiscItemContribution, + addVaultMiscItems, + getGuildForRequestEx +} from "@/src/services/guildService"; import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -25,14 +29,8 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { if (request.MiscItems.length) { addVaultMiscItems(guild, request.MiscItems); - guildMember.MiscItemsContributed ??= []; for (const item of request.MiscItems) { - const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); - if (miscItemContribution) { - miscItemContribution.ItemCount += item.ItemCount; - } else { - guildMember.MiscItemsContributed.push(item); - } + addGuildMemberMiscItemContribution(guildMember, item); addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 35b4d626..08909b08 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,6 @@ import { RequestHandler } from "express"; import { + addGuildMemberMiscItemContribution, getGuildForRequestEx, getGuildVault, hasAccessToDojo, @@ -146,8 +147,7 @@ export const guildTechController: RequestHandler = async (req, res) => { ItemCount: miscItem.ItemCount * -1 }); - guildMember.MiscItemsContributed ??= []; - guildMember.MiscItemsContributed.push(miscItem); + addGuildMemberMiscItemContribution(guildMember, miscItem); } } addMiscItems(inventory, miscItemChanges); diff --git a/src/controllers/api/postGuildAdvertisementController.ts b/src/controllers/api/postGuildAdvertisementController.ts new file mode 100644 index 00000000..e33db645 --- /dev/null +++ b/src/controllers/api/postGuildAdvertisementController.ts @@ -0,0 +1,75 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { GuildAd, GuildMember } from "@/src/models/guildModel"; +import { + addGuildMemberMiscItemContribution, + addVaultMiscItems, + getGuildForRequestEx, + getVaultMiscItemCount, + hasGuildPermissionEx +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +export const postGuildAdvertisementController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId MiscItems"); + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!; + if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) { + res.status(400).end(); + return; + } + const payload = getJSONfromString(String(req.body)); + + // Handle resource cost + const vendor = preprocessVendorManifest( + getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! + ); + const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!; + if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) { + addVaultMiscItems(guild, [ + { + ItemType: offer.ItemPrices![0].ItemType, + ItemCount: offer.ItemPrices![0].ItemCount * -1 + } + ]); + } else { + const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType); + if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) { + res.status(400).json("Insufficient funds"); + return; + } + miscItem.ItemCount -= offer.ItemPrices![0].ItemCount; + addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]); + await guildMember.save(); + await inventory.save(); + } + + // Create or update ad + await GuildAd.findOneAndUpdate( + { GuildId: guild._id }, + { + Emblem: guild.Emblem, + Expiry: new Date(Date.now() + 12 * 3600 * 1000), + Features: payload.Features, + GuildName: guild.Name, + MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }), + RecruitMsg: payload.RecruitMsg, + Tier: guild.Tier + }, + { upsert: true } + ); + + res.end(); +}; + +interface IPostGuildAdvertisementRequest { + Features: number; + RecruitMsg: string; + Languages: string[]; + PurchaseParams: IPurchaseParams; +} diff --git a/src/controllers/dynamic/getGuildAdsController.ts b/src/controllers/dynamic/getGuildAdsController.ts new file mode 100644 index 00000000..1dbe8217 --- /dev/null +++ b/src/controllers/dynamic/getGuildAdsController.ts @@ -0,0 +1,26 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { GuildAd } from "@/src/models/guildModel"; +import { IGuildAdInfoClient } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const getGuildAdsController: RequestHandler = async (req, res) => { + const ads = await GuildAd.find(req.query.tier ? { Tier: req.query.tier } : {}); + const guildAdInfos: IGuildAdInfoClient[] = []; + for (const ad of ads) { + guildAdInfos.push({ + _id: toOid(ad.GuildId), + CrossPlatformEnabled: true, + Emblem: ad.Emblem, + Expiry: toMongoDate(ad.Expiry), + Features: ad.Features, + GuildName: ad.GuildName, + MemberCount: ad.MemberCount, + OriginalPlatform: 0, + RecruitMsg: ad.RecruitMsg, + Tier: ad.Tier + }); + } + res.json({ + GuildAdInfos: guildAdInfos + }); +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 11556893..cf2d1d07 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -10,7 +10,8 @@ import { IGuildLogRoomChange, IGuildLogEntryRoster, IGuildLogEntryContributable, - IDojoLeaderboardEntry + IDojoLeaderboardEntry, + IGuildAdDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -165,6 +166,7 @@ const guildSchema = new Schema( Ranks: { type: [guildRankSchema], default: defaultRanks }, TradeTax: { type: Number, default: 0 }, Tier: { type: Number, default: 1 }, + Emblem: { type: Boolean }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, @@ -225,3 +227,19 @@ const guildMemberSchema = new Schema({ guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true }); export const GuildMember = model("GuildMember", guildMemberSchema); + +const guildAdSchema = new Schema({ + GuildId: { type: Schema.Types.ObjectId, required: true }, + Emblem: Boolean, + Expiry: { type: Date, required: true }, + Features: { type: Number, required: true }, + GuildName: { type: String, required: true }, + MemberCount: { type: Number, required: true }, + RecruitMsg: { type: String, required: true }, + Tier: { type: Number, required: true } +}); + +guildAdSchema.index({ GuildId: 1 }, { unique: true }); +guildAdSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); + +export const GuildAd = model("GuildAd", guildAdSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index bb1404f5..970bc503 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -9,6 +9,7 @@ import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonContro import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; import { artifactTransmutationController } from "@/src/controllers/api/artifactTransmutationController"; +import { cancelGuildAdvertisementController } from "@/src/controllers/api/cancelGuildAdvertisementController"; import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; import { changeGuildRankController } from "@/src/controllers/api/changeGuildRankController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; @@ -80,6 +81,7 @@ import { nameWeaponController } from "@/src/controllers/api/nameWeaponController import { nemesisController } from "@/src/controllers/api/nemesisController"; import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; +import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; @@ -131,6 +133,7 @@ const apiRouter = express.Router(); // get apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); +apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController); apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); @@ -228,6 +231,7 @@ apiRouter.post("/nameWeapon.php", nameWeaponController); apiRouter.post("/nemesis.php", nemesisController); apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); +apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts index 14185e22..fb24fe58 100644 --- a/src/routes/dynamic.ts +++ b/src/routes/dynamic.ts @@ -1,11 +1,13 @@ import express from "express"; import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; +import { getGuildAdsController } from "@/src/controllers/dynamic/getGuildAdsController"; import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController"; import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; const dynamicController = express.Router(); dynamicController.get("/aggregateSessions.php", aggregateSessionsController); +dynamicController.get("/getGuildAds.php", getGuildAdsController); dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController); dynamicController.get("/worldState.php", worldStateController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 19bbf803..de4400b9 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,7 +1,7 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addRecipes, getInventory } from "@/src/services/inventoryService"; -import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { GuildPermission, @@ -298,6 +298,10 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon } }; +export const getVaultMiscItemCount = (guild: TGuildDatabaseDocument, itemType: string): number => { + return guild.VaultMiscItems?.find(x => x.ItemType == itemType)?.ItemCount ?? 0; +}; + export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => { guild.VaultMiscItems ??= []; for (const miscItem of miscItems) { @@ -310,6 +314,16 @@ export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITyp } }; +export const addGuildMemberMiscItemContribution = (guildMember: IGuildMemberDatabase, item: ITypeCount): void => { + guildMember.MiscItemsContributed ??= []; + const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); + if (miscItemContribution) { + miscItemContribution.ItemCount += item.ItemCount; + } else { + guildMember.MiscItemsContributed.push(item); + } +}; + export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { if (build.guildXpValue) { guild.ClaimedXP ??= []; @@ -513,4 +527,6 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise => { contextInfo: guildId.toString(), acceptAction: "GUILD_INVITE" }); + + await GuildAd.deleteOne({ GuildId: guildId }); }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index ab4d1c06..a5a64f87 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -28,6 +28,7 @@ export interface IGuildDatabase { Ranks: IGuildRank[]; TradeTax: number; Tier: number; + Emblem?: boolean; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; @@ -223,3 +224,27 @@ export interface IDojoLeaderboardEntry { r: number; // rank n: string; // displayName } + +export interface IGuildAdInfoClient { + _id: IOid; // Guild ID + CrossPlatformEnabled: boolean; + Emblem?: boolean; + Expiry: IMongoDate; + Features: number; + GuildName: string; + MemberCount: number; + OriginalPlatform: number; + RecruitMsg: string; + Tier: number; +} + +export interface IGuildAdDatabase { + GuildId: Types.ObjectId; + Emblem?: boolean; + Expiry: Date; + Features: number; + GuildName: string; + MemberCount: number; + RecruitMsg: string; + Tier: number; +} From d3d966a50304af1a3034cac109a244499ca0f799 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:14:20 -0700 Subject: [PATCH 320/776] feat: grustrag bolt (#1392) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1392 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/claimCompletedRecipeController.ts | 15 +++++++--- src/controllers/api/startRecipeController.ts | 2 ++ src/models/inventoryModels/inventoryModel.ts | 11 ++++++-- src/services/missionInventoryUpdateService.ts | 28 ++++++++++++++++++- src/types/inventoryTypes/inventoryTypes.ts | 6 +++- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index e8c1b4fb..2d3c480f 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -99,6 +99,11 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); } + } else if (recipe.secretIngredientAction == "SIA_UNBRAND") { + inventory.BrandedSuits!.splice( + inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), + 1 + ); } let InventoryChanges = {}; @@ -116,10 +121,12 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ...updateCurrency(inventory, recipe.skipBuildTimePrice, true) }; } - InventoryChanges = { - ...InventoryChanges, - ...(await addItem(inventory, recipe.resultType, recipe.num, false)) - }; + if (recipe.secretIngredientAction != "SIA_UNBRAND") { + InventoryChanges = { + ...InventoryChanges, + ...(await addItem(inventory, recipe.resultType, recipe.num, false)) + }; + } await inventory.save(); res.json({ InventoryChanges }); } diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index efdcb292..b313771a 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -111,6 +111,8 @@ export const startRecipeController: RequestHandler = async (req, res) => { inventory.PendingSpectreLoadouts.push(spectreLoadout); logger.debug("pending spectre loadout", spectreLoadout); } + } else if (recipe.secretIngredientAction == "SIA_UNBRAND") { + pr.SuitToUnbrand = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length + 0]); } await inventory.save(); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 8e7d42c1..ecd007be 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -903,7 +903,8 @@ const pendingRecipeSchema = new Schema( CompletionDate: Date, LongGuns: { type: [EquipmentSchema], default: undefined }, Pistols: { type: [EquipmentSchema], default: undefined }, - Melee: { type: [EquipmentSchema], default: undefined } + Melee: { type: [EquipmentSchema], default: undefined }, + SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined } }, { id: false } ); @@ -920,6 +921,7 @@ pendingRecipeSchema.set("toJSON", { delete returnedObject.LongGuns; delete returnedObject.Pistols; delete returnedObject.Melees; + delete returnedObject.SuitToUnbrand; (returnedObject as IPendingRecipeClient).CompletionDate = { $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } }; @@ -1484,7 +1486,9 @@ const inventorySchema = new Schema( EchoesHexConquestHardModeStatus: { type: Number, default: undefined }, EchoesHexConquestCacheScoreMission: { type: Number, default: undefined }, EchoesHexConquestActiveFrameVariants: { type: [String], default: undefined }, - EchoesHexConquestActiveStickers: { type: [String], default: undefined } + EchoesHexConquestActiveStickers: { type: [String], default: undefined }, + + BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); @@ -1516,6 +1520,9 @@ inventorySchema.set("toJSON", { if (inventoryDatabase.EntratiVaultCountResetDate) { inventoryResponse.EntratiVaultCountResetDate = toMongoDate(inventoryDatabase.EntratiVaultCountResetDate); } + if (inventoryDatabase.BrandedSuits) { + inventoryResponse.BrandedSuits = inventoryDatabase.BrandedSuits.map(toOid); + } } }); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index ef66afbf..46134b43 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -30,7 +30,7 @@ import { updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; -import { HydratedDocument } from "mongoose"; +import { HydratedDocument, Types } from "mongoose"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getLevelKeyRewards, getNode, toStoreItem } from "@/src/services/itemDataService"; import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; @@ -45,6 +45,7 @@ import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePe import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; import { getInfNodes } from "@/src/helpers/nemesisHelpers"; +import { Loadout } from "../models/inventoryModels/loadoutModel"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -89,6 +90,31 @@ export const addMissionInventoryUpdates = async ( if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } + if ( + inventoryUpdates.MissionFailed && + inventoryUpdates.MissionStatus == "GS_FAILURE" && + inventoryUpdates.EndOfMatchUpload && + inventoryUpdates.ObjectiveReached + ) { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; + const SuitId = new Types.ObjectId(config.s!.ItemId.$oid); + + inventory.BrandedSuits ??= []; + if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) { + inventory.BrandedSuits.push(SuitId); + + await createMessage(inventory.accountOwnerId.toString(), [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + msg: "/Lotus/Language/G1Quests/BrandedMessage", + sub: "/Lotus/Language/G1Quests/BrandedTitle", + att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], + highPriority: true // I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. + } + ]); + } + } for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { if (value === undefined) { logger.error(`Inventory update key ${key} has no value `); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 6f258e13..b4d60f20 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -44,6 +44,7 @@ export interface IInventoryDatabase | "NextRefill" | "Nemesis" | "EntratiVaultCountResetDate" + | "BrandedSuits" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -73,6 +74,7 @@ export interface IInventoryDatabase NextRefill?: Date; Nemesis?: INemesisDatabase; EntratiVaultCountResetDate?: Date; + BrandedSuits?: Types.ObjectId[]; } export interface IQuestKeyDatabase { @@ -346,6 +348,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EchoesHexConquestCacheScoreMission?: number; EchoesHexConquestActiveFrameVariants?: string[]; EchoesHexConquestActiveStickers?: string[]; + BrandedSuits?: IOid[]; } export interface IAffiliation { @@ -857,10 +860,11 @@ export interface IPendingRecipeDatabase { LongGuns?: IEquipmentDatabase[]; Pistols?: IEquipmentDatabase[]; Melee?: IEquipmentDatabase[]; + SuitToUnbrand?: Types.ObjectId; } export interface IPendingRecipeClient - extends Omit { + extends Omit { CompletionDate: IMongoDate; } From 23f8901505a081101298be5505fe2f22d37a2fb4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:14:35 -0700 Subject: [PATCH 321/776] fix: reduce platinum cost of rushing recipes based on progress (#1393) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1393 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/claimCompletedRecipeController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 2d3c480f..f95a950f 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -116,9 +116,15 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ]); } if (req.query.rush) { + const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000); + const start = end - recipe.buildTime; + const secondsElapsed = Math.trunc(Date.now() / 1000) - start; + const progress = secondsElapsed / recipe.buildTime; + logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`); + const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))); InventoryChanges = { ...InventoryChanges, - ...updateCurrency(inventory, recipe.skipBuildTimePrice, true) + ...updateCurrency(inventory, cost, true) }; } if (recipe.secretIngredientAction != "SIA_UNBRAND") { From b0f0b61d49995bd42cad1e1b85a0cf2317828126 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:15:00 -0700 Subject: [PATCH 322/776] fix: allow completion of unknown nodes (#1395) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1395 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/itemDataService.ts | 12 ------------ src/services/missionInventoryUpdateService.ts | 19 ++++++++++--------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 27d23616..7f91b57e 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -24,7 +24,6 @@ import { ExportGear, ExportKeys, ExportRecipes, - ExportRegions, ExportResources, ExportSentinels, ExportWarframes, @@ -34,7 +33,6 @@ import { IMissionReward, IPowersuit, IRecipe, - IRegion, TReward } from "warframe-public-export-plus"; import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; @@ -192,16 +190,6 @@ export const getLevelKeyRewards = ( }; }; -export const getNode = (nodeName: string): IRegion => { - const node = ExportRegions[nodeName]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!node) { - throw new Error(`Node ${nodeName} not found`); - } - - return node; -}; - export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { if (questKey in questCompletionItems) { return questCompletionItems[questKey as keyof typeof questCompletionItems]; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 46134b43..b885c046 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -4,6 +4,7 @@ import { ExportRegions, ExportRewards, IMissionReward as IMissionRewardExternal, + IRegion, IReward } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; @@ -32,7 +33,7 @@ import { import { updateQuestKey } from "@/src/services/questService"; import { HydratedDocument, Types } from "mongoose"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { getLevelKeyRewards, getNode, toStoreItem } from "@/src/services/itemDataService"; +import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; @@ -469,15 +470,15 @@ export const addMissionRewards = async ( } } - if ( - missions && - missions.Tag != "" // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013 - ) { - const node = getNode(missions.Tag); + // ignoring tags not in ExportRegions, because it can just be garbage: + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013 + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 + if (missions && missions.Tag in ExportRegions) { + const node = ExportRegions[missions.Tag]; //node based credit rewards for mission completion if (node.missionIndex !== 28) { - const levelCreditReward = getLevelCreditRewards(missions.Tag); + const levelCreditReward = getLevelCreditRewards(node); missionCompletionCredits += levelCreditReward; inventory.RegularCredits += levelCreditReward; logger.debug(`levelCreditReward ${levelCreditReward}`); @@ -660,8 +661,8 @@ export const addFixedLevelRewards = ( return missionBonusCredits; }; -function getLevelCreditRewards(nodeName: string): number { - const minEnemyLevel = getNode(nodeName).minEnemyLevel; +function getLevelCreditRewards(node: IRegion): number { + const minEnemyLevel = node.minEnemyLevel; return 1000 + (minEnemyLevel - 1) * 100; From 04d39ed9731af998ef37dfb0c99c058b2aa74f52 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:15:32 -0700 Subject: [PATCH 323/776] chore: use SubdocumentArray.id in some more places (#1400) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1400 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/evolveWeaponController.ts | 4 ++-- src/controllers/api/focusController.ts | 20 +++++++++---------- .../api/infestedFoundryController.ts | 4 ++-- src/controllers/api/nameWeaponController.ts | 4 +--- .../api/setWeaponSkillTreeController.ts | 6 ++---- .../popArchonCrystalUpgradeController.ts | 2 +- .../pushArchonCrystalUpgradeController.ts | 2 +- src/services/saveLoadoutService.ts | 4 +--- src/services/shipCustomizationsService.ts | 12 +++++------ 9 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/controllers/api/evolveWeaponController.ts b/src/controllers/api/evolveWeaponController.ts index 3b2550bb..395b3340 100644 --- a/src/controllers/api/evolveWeaponController.ts +++ b/src/controllers/api/evolveWeaponController.ts @@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => { recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 })) ); - const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!; + const item = inventory[payload.Category].id(req.query.ItemId as string)!; item.Features ??= 0; item.Features |= EquipmentFeatures.INCARNON_GENESIS; @@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => { } ]); - const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!; + const item = inventory[payload.Category].id(req.query.ItemId as string)!; item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS; } else { throw new Error(`unexpected evolve weapon action: ${payload.Action}`); diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index a6c1c59c..0d2b3f65 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -18,17 +18,15 @@ export const focusController: RequestHandler = async (req, res) => { case FocusOperation.InstallLens: { const request = JSON.parse(String(req.body)) as ILensInstallRequest; const inventory = await getInventory(accountId); - for (const item of inventory[request.Category]) { - if (item._id.toString() == request.WeaponId) { - item.FocusLens = request.LensType; - addMiscItems(inventory, [ - { - ItemType: request.LensType, - ItemCount: -1 - } satisfies IMiscItem - ]); - break; - } + const item = inventory[request.Category].id(request.WeaponId); + if (item) { + item.FocusLens = request.LensType; + addMiscItems(inventory, [ + { + ItemType: request.LensType, + ItemCount: -1 + } satisfies IMiscItem + ]); } await inventory.save(); res.json({ diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index 0e3b4ed7..d63c2203 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -28,7 +28,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { // shard installation const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; + const suit = inventory.Suits.id(request.SuitId.$oid)!; if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; } @@ -56,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { // shard removal const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; + const suit = inventory.Suits.id(request.SuitId.$oid)!; const miscItemChanges: IMiscItem[] = []; if (suit.ArchonCrystalUpgrades![request.Slot].Color) { diff --git a/src/controllers/api/nameWeaponController.ts b/src/controllers/api/nameWeaponController.ts index 843feeb9..f4fc88a7 100644 --- a/src/controllers/api/nameWeaponController.ts +++ b/src/controllers/api/nameWeaponController.ts @@ -12,9 +12,7 @@ export const nameWeaponController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); const body = getJSONfromString(String(req.body)); - const item = inventory[req.query.Category as string as TEquipmentKey].find( - item => item._id.toString() == (req.query.ItemId as string) - )!; + const item = inventory[req.query.Category as string as TEquipmentKey].id(req.query.ItemId as string)!; if (body.ItemName != "") { item.ItemName = body.ItemName; } else { diff --git a/src/controllers/api/setWeaponSkillTreeController.ts b/src/controllers/api/setWeaponSkillTreeController.ts index 98fe7652..31323adc 100644 --- a/src/controllers/api/setWeaponSkillTreeController.ts +++ b/src/controllers/api/setWeaponSkillTreeController.ts @@ -6,12 +6,10 @@ import { WeaponTypeInternal } from "@/src/services/itemDataService"; export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, req.query.Category as string); const payload = getJSONfromString(String(req.body)); - const item = inventory[req.query.Category as WeaponTypeInternal].find( - item => item._id.toString() == (req.query.ItemId as string) - )!; + const item = inventory[req.query.Category as WeaponTypeInternal].id(req.query.ItemId as string)!; item.SkillTree = payload.SkillTree; await inventory.save(); diff --git a/src/controllers/custom/popArchonCrystalUpgradeController.ts b/src/controllers/custom/popArchonCrystalUpgradeController.ts index c9c84b85..34e87ec6 100644 --- a/src/controllers/custom/popArchonCrystalUpgradeController.ts +++ b/src/controllers/custom/popArchonCrystalUpgradeController.ts @@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService"; export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string)); + const suit = inventory.Suits.id(req.query.oid as string); if (suit && suit.ArchonCrystalUpgrades) { suit.ArchonCrystalUpgrades = suit.ArchonCrystalUpgrades.filter( x => x.UpgradeType != (req.query.type as string) diff --git a/src/controllers/custom/pushArchonCrystalUpgradeController.ts b/src/controllers/custom/pushArchonCrystalUpgradeController.ts index 093b0678..3a9286ee 100644 --- a/src/controllers/custom/pushArchonCrystalUpgradeController.ts +++ b/src/controllers/custom/pushArchonCrystalUpgradeController.ts @@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService"; export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string)); + const suit = inventory.Suits.id(req.query.oid as string); if (suit) { suit.ArchonCrystalUpgrades ??= []; const count = (req.query.count as number | undefined) ?? 1; diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 038f0d23..2f8711c6 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -84,9 +84,7 @@ export const handleInventoryItemConfigChange = async ( continue; } - const oldLoadoutConfig = loadout[loadoutSlot].find( - loadout => loadout._id.toString() === loadoutId - ); + const oldLoadoutConfig = loadout[loadoutSlot].id(loadoutId); const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; const loadoutConfigDatabase: ILoadoutConfigDatabase = { diff --git a/src/services/shipCustomizationsService.ts b/src/services/shipCustomizationsService.ts index bc7ca5fb..20c3d4ca 100644 --- a/src/services/shipCustomizationsService.ts +++ b/src/services/shipCustomizationsService.ts @@ -61,19 +61,17 @@ export const handleSetShipDecorations = async ( if (placedDecoration.MoveId) { //moved within the same room if (placedDecoration.OldRoom === placedDecoration.Room) { - const existingDecorationIndex = roomToPlaceIn.PlacedDecos.findIndex( - deco => deco._id.toString() === placedDecoration.MoveId - ); + const existingDecoration = roomToPlaceIn.PlacedDecos.id(placedDecoration.MoveId); - if (existingDecorationIndex === -1) { + if (!existingDecoration) { throw new Error("decoration to be moved not found"); } - roomToPlaceIn.PlacedDecos[existingDecorationIndex].Pos = placedDecoration.Pos; - roomToPlaceIn.PlacedDecos[existingDecorationIndex].Rot = placedDecoration.Rot; + existingDecoration.Pos = placedDecoration.Pos; + existingDecoration.Rot = placedDecoration.Rot; if (placedDecoration.Scale) { - roomToPlaceIn.PlacedDecos[existingDecorationIndex].Scale = placedDecoration.Scale; + existingDecoration.Scale = placedDecoration.Scale; } await personalRooms.save(); From 054abee62ca605021b6f1edf2903e92cb0720f57 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:16:09 -0700 Subject: [PATCH 324/776] chore: use inventory projection in sellController (#1399) Yeah, it's not pretty but it's a good amount faster. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1399 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/sellController.ts | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index fa8f3f01..ad31c259 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -13,7 +13,42 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export const sellController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as ISellRequest; const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const requiredFields = new Set(); + if (payload.SellCurrency == "SC_RegularCredits") { + requiredFields.add("RegularCredits"); + } else if (payload.SellCurrency == "SC_FusionPoints") { + requiredFields.add("FusionPoints"); + } else { + requiredFields.add("MiscItems"); + } + for (const key of Object.keys(payload.Items)) { + requiredFields.add(key); + } + if (requiredFields.has("Upgrades")) { + requiredFields.add("RawUpgrades"); + } + if (payload.Items.Suits) { + requiredFields.add(InventorySlot.SUITS); + } + if (payload.Items.LongGuns || payload.Items.Pistols || payload.Items.Melee) { + requiredFields.add(InventorySlot.WEAPONS); + } + if (payload.Items.SpaceSuits) { + requiredFields.add(InventorySlot.SPACESUITS); + } + if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) { + requiredFields.add(InventorySlot.SPACEWEAPONS); + } + if (payload.Items.Sentinels || payload.Items.SentinelWeapons) { + requiredFields.add(InventorySlot.SENTINELS); + } + if (payload.Items.OperatorAmps) { + requiredFields.add(InventorySlot.AMPS); + } + if (payload.Items.Hoverboards) { + requiredFields.add(InventorySlot.SPACESUITS); + } + const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); // Give currency if (payload.SellCurrency == "SC_RegularCredits") { From 9e0dd3e0a5cf0c1f888de6644ec44593ad70a7f0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:26:44 -0700 Subject: [PATCH 325/776] chore: run save operations in parallel where possible (#1401) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1401 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/contributeToDojoComponentController.ts | 4 +--- .../api/contributeToVaultController.ts | 16 +++++++++++----- .../api/dojoComponentRushController.ts | 6 ++---- src/controllers/api/guildTechController.ts | 4 +--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index acdbf8a4..6d0016eb 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -64,9 +64,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r } } - await guild.save(); - await inventory.save(); - await guildMember.save(); + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); res.json({ ...(await getDojoClient(guild, 0, component._id)), InventoryChanges: inventoryChanges diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index 74dc91a4..7960c951 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -4,14 +4,20 @@ import { addVaultMiscItems, getGuildForRequestEx } from "@/src/services/guildService"; -import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { + addFusionTreasures, + addMiscItems, + addShipDecorations, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; export const contributeToVaultController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures"); const guild = await getGuildForRequestEx(req, inventory); const guildMember = (await GuildMember.findOne( { accountId, guildId: guild._id }, @@ -20,6 +26,8 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; if (request.RegularCredits) { + updateCurrency(inventory, request.RegularCredits, false); + guild.VaultRegularCredits ??= 0; guild.VaultRegularCredits += request.RegularCredits; @@ -52,9 +60,7 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { } } - await guild.save(); - await inventory.save(); - await guildMember.save(); + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); res.end(); }; diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts index 33aec126..899ed0af 100644 --- a/src/controllers/api/dojoComponentRushController.ts +++ b/src/controllers/api/dojoComponentRushController.ts @@ -47,13 +47,11 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => { } } - await guild.save(); - await inventory.save(); - const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!; guildMember.PremiumCreditsContributed ??= 0; guildMember.PremiumCreditsContributed += request.Amount; - await guildMember.save(); + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); res.json({ ...(await getDojoClient(guild, 0, component._id)), diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 08909b08..5b0b5374 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -157,9 +157,7 @@ export const guildTechController: RequestHandler = async (req, res) => { // Check if research is fully funded now. await processGuildTechProjectContributionsUpdate(guild, techProject); - await guild.save(); - await inventory.save(); - await guildMember.save(); + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); res.json({ InventoryChanges: inventoryChanges, Vault: getGuildVault(guild) From 42e08faaaf338c799cc14cd0fc06f5321095da9e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:26:55 -0700 Subject: [PATCH 326/776] chore: handle account switching guilds (#1398) Plus some additional inventory cleanup when a guild is being deleted forcefully. Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/confirmGuildInvitationController.ts | 51 ++++++++++------ src/controllers/api/createGuildController.ts | 17 ++++-- .../api/removeFromGuildController.ts | 19 +----- src/services/guildService.ts | 60 ++++++++++++------- 4 files changed, 87 insertions(+), 60 deletions(-) diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts index 2685be99..c03a4285 100644 --- a/src/controllers/api/confirmGuildInvitationController.ts +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -1,23 +1,47 @@ import { Guild, GuildMember } from "@/src/models/guildModel"; -import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService"; +import { deleteGuild, getGuildClient, removeDojoKeyItems } from "@/src/services/guildService"; +import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; import { Types } from "mongoose"; export const confirmGuildInvitationController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); - const guildMember = await GuildMember.findOne({ + const invitedGuildMember = await GuildMember.findOne({ accountId: account._id, guildId: req.query.clanId as string }); - if (guildMember) { - guildMember.status = 0; - await guildMember.save(); + if (invitedGuildMember) { + let inventoryChanges: IInventoryChanges = {}; - await updateInventoryForConfirmedGuildJoin( - account._id.toString(), - new Types.ObjectId(req.query.clanId as string) - ); + // If this account is already in a guild, we need to do cleanup first. + const guildMember = await GuildMember.findOneAndDelete({ accountId: account._id, status: 0 }); + if (guildMember) { + const inventory = await getInventory(account._id.toString(), "LevelKeys Recipes"); + inventoryChanges = removeDojoKeyItems(inventory); + await inventory.save(); + + if (guildMember.rank == 0) { + await deleteGuild(guildMember.guildId); + } + } + + // Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates. + invitedGuildMember.status = 0; + await invitedGuildMember.save(); + + const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); + inventory.GuildId = new Types.ObjectId(req.query.clanId as string); + const recipeChanges = [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]; + addRecipes(inventory, recipeChanges); + combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges }); + await inventory.save(); const guild = (await Guild.findById(req.query.clanId as string))!; @@ -31,14 +55,7 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res) res.json({ ...(await getGuildClient(guild, account._id.toString())), - InventoryChanges: { - Recipes: [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ] - } + InventoryChanges: inventoryChanges }); } else { res.end(); diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index d8757545..4d3e21c9 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -2,11 +2,8 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Guild, GuildMember } from "@/src/models/guildModel"; -import { - createUniqueClanName, - getGuildClient, - updateInventoryForConfirmedGuildJoin -} from "@/src/services/guildService"; +import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; +import { addRecipes, getInventory } from "@/src/services/inventoryService"; export const createGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -26,7 +23,15 @@ export const createGuildController: RequestHandler = async (req, res) => { rank: 0 }); - await updateInventoryForConfirmedGuildJoin(accountId, guild._id); + const inventory = await getInventory(accountId, "GuildId Recipes"); + inventory.GuildId = guild._id; + addRecipes(inventory, [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]); + await inventory.save(); res.json({ ...(await getGuildClient(guild, accountId)), diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 705d597f..3571e1e1 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -1,7 +1,7 @@ import { GuildMember } from "@/src/models/guildModel"; import { Inbox } from "@/src/models/inboxModel"; import { Account } from "@/src/models/loginModel"; -import { deleteGuild, getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; +import { deleteGuild, getGuildForRequest, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; @@ -22,22 +22,9 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { await deleteGuild(guild._id); } else { if (guildMember.status == 0) { - const inventory = await getInventory(payload.userId); + const inventory = await getInventory(payload.userId, "GuildId LevelKeys Recipes"); inventory.GuildId = undefined; - - // Remove clan key or blueprint from kicked member - const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); - if (itemIndex != -1) { - inventory.LevelKeys.splice(itemIndex, 1); - } else { - const recipeIndex = inventory.Recipes.findIndex( - x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint" - ); - if (recipeIndex != -1) { - inventory.Recipes.splice(recipeIndex, 1); - } - } - + removeDojoKeyItems(inventory); await inventory.save(); } else if (guildMember.status == 2) { // Delete the inbox message for the invite diff --git a/src/services/guildService.ts b/src/services/guildService.ts index de4400b9..658b0f27 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,6 +1,6 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addRecipes, getInventory } from "@/src/services/inventoryService"; +import { getInventory } from "@/src/services/inventoryService"; import { Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { @@ -25,6 +25,7 @@ import { Account } from "../models/loginModel"; import { getRandomInt } from "./rngService"; import { Inbox } from "../models/inboxModel"; import { ITypeCount } from "../types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "../types/purchaseTypes"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -350,26 +351,6 @@ export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClie member.ActiveAvatarImageType = inventory.ActiveAvatarImageType; }; -export const updateInventoryForConfirmedGuildJoin = async ( - accountId: string, - guildId: Types.ObjectId -): Promise => { - const inventory = await getInventory(accountId, "GuildId Recipes"); - - // Set GuildId - inventory.GuildId = guildId; - - // Give clan key blueprint - addRecipes(inventory, [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ]); - - await inventory.save(); -}; - export const createUniqueClanName = async (name: string): Promise => { const initialDiscriminator = getRandomInt(0, 999); let discriminator = initialDiscriminator; @@ -518,8 +499,45 @@ const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Pro } }; +export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => { + const inventoryChanges: IInventoryChanges = {}; + + const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); + if (itemIndex != -1) { + inventoryChanges.LevelKeys = [ + { + ItemType: "/Lotus/Types/Keys/DojoKey", + ItemCount: inventory.LevelKeys[itemIndex].ItemCount * -1 + } + ]; + inventory.LevelKeys.splice(itemIndex, 1); + } + + const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); + if (recipeIndex != -1) { + inventoryChanges.Recipes = [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: inventory.Recipes[recipeIndex].ItemCount * -1 + } + ]; + inventory.Recipes.splice(recipeIndex, 1); + } + + return inventoryChanges; +}; + export const deleteGuild = async (guildId: Types.ObjectId): Promise => { await Guild.deleteOne({ _id: guildId }); + + const guildMembers = await GuildMember.find({ guildId, status: 0 }, "accountId"); + for (const member of guildMembers) { + const inventory = await getInventory(member.accountId.toString(), "GuildId LevelKeys Recipes"); + inventory.GuildId = undefined; + removeDojoKeyItems(inventory); + await inventory.save(); + } + await GuildMember.deleteMany({ guildId }); // If guild sent any invites, delete those inbox messages as well. From 916252296255b7cb49dff2838ecc264402fd549c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:51:12 -0700 Subject: [PATCH 327/776] chore: update PE+ (#1407) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1407 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00f60a01..5e4ec305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.48", + "warframe-public-export-plus": "^0.5.49", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3762,9 +3762,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.48.tgz", - "integrity": "sha512-vJitVYnaViQo43xAkL/h3MJ/6wS7YknKEYhYs+N/GrsspYLMPGf9KSuR19tprB2g9KVGS5o67t0v5K8p0RTQCQ==" + "version": "0.5.49", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.49.tgz", + "integrity": "sha512-11HA8qEMhFfl12W2qIjjk7fhas+/5G2yXbrOEb8FRZby6tWka0CyUnB6tLT+PCqBEIoU+kwhz0g7CLh3Zmy7Pw==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 15c4cbfe..f0b3c4cc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.48", + "warframe-public-export-plus": "^0.5.49", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" From a0fa41cd58971480903963a8bcb98b326b15bc1c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:18:00 -0700 Subject: [PATCH 328/776] chore: accept ObjectId for accountId when sending inbox messages (#1409) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1409 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/contributeGuildClassController.ts | 2 +- src/controllers/api/giftingController.ts | 2 +- src/controllers/api/inboxController.ts | 2 +- src/services/inboxService.ts | 6 +++--- src/services/inventoryService.ts | 2 +- src/services/missionInventoryUpdateService.ts | 12 ++++++------ src/services/questService.ts | 8 ++++---- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index 854f5223..1a20f4ef 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -41,7 +41,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) = const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId"); for (const member of members) { // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". - await createMessage(member.accountId.toString(), [ + await createMessage(member.accountId, [ { sndr: guild.Name, msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts index ce3e5a30..f965e60c 100644 --- a/src/controllers/api/giftingController.ts +++ b/src/controllers/api/giftingController.ts @@ -56,7 +56,7 @@ export const giftingController: RequestHandler = async (req, res) => { await senderInventory.save(); const senderName = getSuffixedName(senderAccount); - await createMessage(account._id.toString(), [ + await createMessage(account._id, [ { sndr: senderName, msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage", diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 84ebf03e..7cb777e2 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -67,7 +67,7 @@ export const inboxController: RequestHandler = async (req, res) => { (await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges ); if (sender) { - await createMessage(sender._id.toString(), [ + await createMessage(sender._id, [ { sndr: recipientName, msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody", diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index 49b5cca3..0c2d698d 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -1,6 +1,6 @@ import { IMessageDatabase, Inbox } from "@/src/models/inboxModel"; import { getAccountForRequest } from "@/src/services/loginService"; -import { HydratedDocument } from "mongoose"; +import { HydratedDocument, Types } from "mongoose"; import { Request } from "express"; import eventMessages from "@/static/fixed_responses/eventMessages.json"; import { logger } from "@/src/utils/logger"; @@ -39,7 +39,7 @@ export const createNewEventMessages = async (req: Request): Promise => { return; } - const savedEventMessages = await createMessage(account._id.toString(), newEventMessages); + const savedEventMessages = await createMessage(account._id, newEventMessages); logger.debug("created event messages", savedEventMessages); const latestEventMessage = newEventMessages.reduce((prev, current) => @@ -50,7 +50,7 @@ export const createNewEventMessages = async (req: Request): Promise => { await account.save(); }; -export const createMessage = async (accountId: string, messages: IMessageCreationTemplate[]) => { +export const createMessage = async (accountId: string | Types.ObjectId, messages: IMessageCreationTemplate[]) => { const ownerIdMessages = messages.map(m => ({ ...m, ownerId: accountId diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 92543e6f..f296e43f 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1019,7 +1019,7 @@ export const addEmailItem = async ( const meta = ExportEmailItems[typeName]; const emailItem = inventory.EmailItems.find(x => x.ItemType == typeName); if (!emailItem || !meta.sendOnlyOnce) { - await createMessage(inventory.accountOwnerId.toString(), [convertInboxMessage(meta.message)]); + await createMessage(inventory.accountOwnerId, [convertInboxMessage(meta.message)]); if (emailItem) { emailItem.ItemCount += 1; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b885c046..f4fe1164 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -105,13 +105,13 @@ export const addMissionInventoryUpdates = async ( if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) { inventory.BrandedSuits.push(SuitId); - await createMessage(inventory.accountOwnerId.toString(), [ + await createMessage(inventory.accountOwnerId, [ { sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", msg: "/Lotus/Language/G1Quests/BrandedMessage", sub: "/Lotus/Language/G1Quests/BrandedTitle", att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], - highPriority: true // I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. + highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. } ]); } @@ -292,14 +292,14 @@ export const addMissionInventoryUpdates = async ( if (gate.complete && !gate.sent) { gate.sent = true; if (gate.threshold == 0.5) { - await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage50]); + await createMessage(inventory.accountOwnerId, [kuriaMessage50]); } else { - await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage75]); + await createMessage(inventory.accountOwnerId, [kuriaMessage75]); } } } if (progress >= 1.0) { - await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage100]); + await createMessage(inventory.accountOwnerId, [kuriaMessage100]); } } } else { @@ -338,7 +338,7 @@ export const addMissionInventoryUpdates = async ( for (const deathMark of value) { if (!inventory.DeathMarks.find(x => x == deathMark)) { // It's a new death mark; we have to say the line. - await createMessage(inventory.accountOwnerId.toString(), [ + await createMessage(inventory.accountOwnerId, [ { sub: "/Lotus/Language/G1Quests/DeathMarkTitle", sndr: "/Lotus/Language/G1Quests/DeathMarkSender", diff --git a/src/services/questService.ts b/src/services/questService.ts index 2e32ea1d..a8a20629 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -16,7 +16,7 @@ import { IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; -import { HydratedDocument } from "mongoose"; +import { HydratedDocument, Types } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; import { addFixedLevelRewards } from "./missionInventoryUpdateService"; import { IInventoryChanges } from "../types/purchaseTypes"; @@ -107,7 +107,7 @@ export const addQuestKey = ( } if (questKey.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain") { - void createMessage(inventory.accountOwnerId.toString(), [ + void createMessage(inventory.accountOwnerId, [ { sndr: "/Lotus/Language/Bosses/Loid", icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", @@ -166,7 +166,7 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest } if (chainStages[i].messageToSendWhenTriggered) { - await giveKeyChainMessage(inventory, inventory.accountOwnerId.toString(), { + await giveKeyChainMessage(inventory, inventory.accountOwnerId, { KeyChain: questKey, ChainStage: i }); @@ -238,7 +238,7 @@ export const giveKeyChainItem = async ( export const giveKeyChainMessage = async ( inventory: TInventoryDatabaseDocument, - accountId: string, + accountId: string | Types.ObjectId, keyChainInfo: IKeyChainRequest ): Promise => { const keyChainMessage = getKeyChainMessage(keyChainInfo); From 3d698286100ac6d1e889ef5f1f6d69d31341be7e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:18:25 -0700 Subject: [PATCH 329/776] fix: give non-exalted additional items when acquiring warframe (#1408) Also upgraded `no-misused-promises` to an error and added `IsNew` field to powersuits. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1408 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .eslintrc | 1 - .../api/giveStartingGearController.ts | 2 +- src/services/inventoryService.ts | 70 +++++++++++++------ src/services/itemDataService.ts | 9 --- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/.eslintrc b/.eslintrc index e77dc45d..c7994fc1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,7 +16,6 @@ "@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], - "@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-assignment": "warn", diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index 93fa1452..74d45e29 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -53,7 +53,7 @@ export const addStartingGear = async ( addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); - addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); + await addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); addEquipment( inventory, "DataKnives", diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index f296e43f..6eba1872 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -30,7 +30,7 @@ import { import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { convertInboxMessage, fromStoreItem, getExalted, getKeyChainItems } from "@/src/services/itemDataService"; +import { convertInboxMessage, fromStoreItem, getKeyChainItems } from "@/src/services/itemDataService"; import { EquipmentFeatures, IEquipmentClient, @@ -55,8 +55,10 @@ import { ExportSentinels, ExportSyndicates, ExportUpgrades, + ExportWarframes, ExportWeapons, IDefaultUpgrade, + IPowersuit, TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; @@ -481,12 +483,12 @@ export const addItem = async ( switch (typeName.substr(1).split("/")[2]) { default: { return { - ...addPowerSuit( + ...(await addPowerSuit( inventory, typeName, {}, premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - ), + )), ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) }; } @@ -504,12 +506,12 @@ export const addItem = async ( } case "EntratiMech": { return { - ...addMechSuit( + ...(await addMechSuit( inventory, typeName, {}, premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - ), + )), ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) }; } @@ -697,40 +699,65 @@ const addSentinelWeapon = ( inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON()); }; -export const addPowerSuit = ( +export const addPowerSuit = async ( inventory: TInventoryDatabaseDocument, powersuitName: string, inventoryChanges: IInventoryChanges = {}, features: number | undefined = undefined -): IInventoryChanges => { - const specialItems = getExalted(powersuitName); - if (specialItems) { - for (const specialItem of specialItems) { - addSpecialItem(inventory, specialItem, inventoryChanges); +): Promise => { + const powersuit = ExportWarframes[powersuitName] as IPowersuit | undefined; + const exalted = powersuit?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + if (powersuit?.additionalItems) { + for (const item of powersuit.additionalItems) { + if (exalted.indexOf(item) == -1) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + } } } const suitIndex = - inventory.Suits.push({ ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0, Features: features }) - 1; + inventory.Suits.push({ + ItemType: powersuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features, + IsNew: true + }) - 1; inventoryChanges.Suits ??= []; inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON()); return inventoryChanges; }; -export const addMechSuit = ( +export const addMechSuit = async ( inventory: TInventoryDatabaseDocument, mechsuitName: string, inventoryChanges: IInventoryChanges = {}, features: number | undefined = undefined -): IInventoryChanges => { - const specialItems = getExalted(mechsuitName); - if (specialItems) { - for (const specialItem of specialItems) { - addSpecialItem(inventory, specialItem, inventoryChanges); +): Promise => { + const powersuit = ExportWarframes[mechsuitName] as IPowersuit | undefined; + const exalted = powersuit?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + if (powersuit?.additionalItems) { + for (const item of powersuit.additionalItems) { + if (exalted.indexOf(item) == -1) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + } } } const suitIndex = - inventory.MechSuits.push({ ItemType: mechsuitName, Configs: [], UpgradeVer: 101, XP: 0, Features: features }) - - 1; + inventory.MechSuits.push({ + ItemType: mechsuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features, + IsNew: true + }) - 1; inventoryChanges.MechSuits ??= []; inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); return inventoryChanges; @@ -768,7 +795,8 @@ export const addSpaceSuit = ( Configs: [], UpgradeVer: 101, XP: 0, - Features: features + Features: features, + IsNew: true }) - 1; inventoryChanges.SpaceSuits ??= []; inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON()); diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 7f91b57e..2fc6c0da 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -31,7 +31,6 @@ import { IDefaultUpgrade, IInboxMessage, IMissionReward, - IPowersuit, IRecipe, TReward } from "warframe-public-export-plus"; @@ -56,10 +55,6 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => { return Object.values(ExportRecipes).find(x => x.resultType == resultType); }; -export const getExalted = (uniqueName: string): string[] | undefined => { - return getSuitByUniqueName(uniqueName)?.exalted; -}; - export const getItemCategoryByUniqueName = (uniqueName: string): string => { //Lotus/Types/Items/MiscItems/PolymerBundle @@ -76,10 +71,6 @@ export const getItemCategoryByUniqueName = (uniqueName: string): string => { return category; }; -export const getSuitByUniqueName = (uniqueName: string): IPowersuit | undefined => { - return ExportWarframes[uniqueName]; -}; - export const getItemName = (uniqueName: string): string | undefined => { if (uniqueName in ExportArcanes) { return ExportArcanes[uniqueName].name; From fb58aeb07fdbfc417e0b98b24c263f7faa315b05 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:18:33 -0700 Subject: [PATCH 330/776] chore: reimplement setWeaponSkillTree as a mongo query (#1406) This is faster by like 1-2 ms Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1406 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/setWeaponSkillTreeController.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/setWeaponSkillTreeController.ts b/src/controllers/api/setWeaponSkillTreeController.ts index 31323adc..a750c3ce 100644 --- a/src/controllers/api/setWeaponSkillTreeController.ts +++ b/src/controllers/api/setWeaponSkillTreeController.ts @@ -1,18 +1,25 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { WeaponTypeInternal } from "@/src/services/itemDataService"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId, req.query.Category as string); const payload = getJSONfromString(String(req.body)); - const item = inventory[req.query.Category as WeaponTypeInternal].id(req.query.ItemId as string)!; - item.SkillTree = payload.SkillTree; + if (equipmentKeys.indexOf(req.query.Category as TEquipmentKey) != -1) { + await Inventory.updateOne( + { + accountOwnerId: accountId, + [`${req.query.Category as string}._id`]: req.query.ItemId as string + }, + { + [`${req.query.Category as string}.$.SkillTree`]: payload.SkillTree + } + ); + } - await inventory.save(); res.end(); }; From d033c2bc12dc89b65302d8ccf704a95ded867314 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:18:41 -0700 Subject: [PATCH 331/776] feat(webui): MoaPets support (#1402) Translations were taken from the game Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1402 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../custom/addModularEquipmentController.ts | 65 ++++++++++++++- src/helpers/modularWeaponHelper.ts | 1 + static/webui/index.html | 37 +++++++++ static/webui/script.js | 80 ++++++++++++++++--- static/webui/translations/de.js | 6 ++ static/webui/translations/en.js | 6 ++ static/webui/translations/fr.js | 6 ++ static/webui/translations/ru.js | 6 ++ static/webui/translations/zh.js | 6 ++ 9 files changed, 201 insertions(+), 12 deletions(-) diff --git a/src/controllers/custom/addModularEquipmentController.ts b/src/controllers/custom/addModularEquipmentController.ts index ea51e2d3..f1f6cd17 100644 --- a/src/controllers/custom/addModularEquipmentController.ts +++ b/src/controllers/custom/addModularEquipmentController.ts @@ -1,15 +1,26 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, addEquipment, occupySlot, productCategoryToInventoryBin } from "@/src/services/inventoryService"; +import { + getInventory, + addEquipment, + occupySlot, + productCategoryToInventoryBin, + applyDefaultUpgrades +} from "@/src/services/inventoryService"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { getDefaultUpgrades } from "@/src/services/itemDataService"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ExportWeapons } from "warframe-public-export-plus"; import { RequestHandler } from "express"; export const addModularEquipmentController: RequestHandler = async (req, res) => { + const requiredFields = new Set(); const accountId = await getAccountIdForRequest(req); const request = req.body as IAddModularEquipmentRequest; const category = modularWeaponTypes[request.ItemType]; const inventoryBin = productCategoryToInventoryBin(category)!; - const inventory = await getInventory(accountId, `${category} ${inventoryBin}`); + requiredFields.add(category); + requiredFields.add(inventoryBin); + request.ModularParts.forEach(part => { if (ExportWeapons[part].gunType) { if (category == "LongGuns") { @@ -25,9 +36,57 @@ export const addModularEquipmentController: RequestHandler = async (req, res) => GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" }[ExportWeapons[part].gunType]; } + } else if (request.ItemType == "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") { + if (part.includes("ZanukaPetPartHead")) { + request.ItemType = { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + }[part]!; + } } }); - addEquipment(inventory, category, request.ItemType, request.ModularParts); + const defaultUpgrades = getDefaultUpgrades(request.ModularParts); + if (defaultUpgrades) { + requiredFields.add("RawUpgrades"); + } + const defaultWeaponsMap: Record = { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIP" + ], + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIS" + ], + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS" + ] + }; + const defaultWeapons = defaultWeaponsMap[request.ItemType]; + if (defaultWeapons) { + for (const defaultWeapon of defaultWeapons) { + const category = ExportWeapons[defaultWeapon].productCategory; + requiredFields.add(category); + requiredFields.add(productCategoryToInventoryBin(category)); + } + } + + const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); + if (defaultWeapons) { + for (const defaultWeapon of defaultWeapons) { + const category = ExportWeapons[defaultWeapon].productCategory; + addEquipment(inventory, category, defaultWeapon); + occupySlot(inventory, productCategoryToInventoryBin(category)!, true); + } + } + + const defaultOverwrites: Partial = { + Configs: applyDefaultUpgrades(inventory, defaultUpgrades) + }; + + addEquipment(inventory, category, request.ItemType, request.ModularParts, undefined, defaultOverwrites); occupySlot(inventory, inventoryBin, true); await inventory.save(); res.end(); diff --git a/src/helpers/modularWeaponHelper.ts b/src/helpers/modularWeaponHelper.ts index cf0e9b19..5651f373 100644 --- a/src/helpers/modularWeaponHelper.ts +++ b/src/helpers/modularWeaponHelper.ts @@ -13,6 +13,7 @@ export const modularWeaponTypes: Record = { "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps", "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards", "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit": "MoaPets", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets", diff --git a/static/webui/index.html b/static/webui/index.html index f5df02df..aefb1f11 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -294,6 +294,34 @@
+
+
+
+
+
+ + +
+ + + + +
+
+
+
+
+
@@ -637,6 +665,7 @@ + @@ -659,6 +688,14 @@ + + + + + + + + diff --git a/static/webui/script.js b/static/webui/script.js index 73783972..9538c218 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -202,6 +202,15 @@ function fetchItemList() { uniqueName: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", name: loc("code_kitgun") }); + data.MoaPets ??= []; + data.MoaPets.push({ + uniqueName: "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + name: loc("code_moa") + }); + data.MoaPets.push({ + uniqueName: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit", + name: loc("code_zanuka") + }); const itemMap = { // Generics for rivens @@ -220,7 +229,16 @@ function fetchItemList() { "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/OperatorTrainingAmpWeapon": { name: loc("code_moteAmp") }, - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kDrive") } + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kDrive") }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": { + name: loc("code_zanukaA") + }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": { + name: loc("code_zanukaB") + }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": { + name: loc("code_zanukaC") + } }; for (const [type, items] of Object.entries(data)) { if (type == "archonCrystalUpgrades") { @@ -259,7 +277,15 @@ function fetchItemList() { "LWPT_GUN_PRIMARY_HANDLE", "LWPT_GUN_SECONDARY_HANDLE", "LWPT_GUN_BARREL", - "LWPT_GUN_CLIP" + "LWPT_GUN_CLIP", + "LWPT_MOA_ENGINE", + "LWPT_MOA_PAYLOAD", + "LWPT_MOA_HEAD", + "LWPT_MOA_LEG", + "LWPT_ZANUKA_BODY", + "LWPT_ZANUKA_HEAD", + "LWPT_ZANUKA_LEG", + "LWPT_ZANUKA_TAIL" ]; if (supportedModularParts.includes(item.partType)) { const option = document.createElement("option"); @@ -269,6 +295,7 @@ function fetchItemList() { .getElementById("datalist-" + type + "-" + item.partType.slice(5)) .appendChild(option); } else { + console.log(item.partType); const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; @@ -308,7 +335,11 @@ function updateInventory() { "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit" + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" ]; // Populate inventory route @@ -330,7 +361,8 @@ function updateInventory() { "SentinelWeapons", "Hoverboards", "OperatorAmps", - "MechSuits" + "MechSuits", + "MoaPets" ].forEach(category => { document.getElementById(category + "-list").innerHTML = ""; data[category].forEach(item => { @@ -709,6 +741,13 @@ function doAcquireModularEquipment(category, ItemType) { case "Pistols": requiredParts = ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"]; break; + case "MoaPets": + if (ItemType == "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { + requiredParts = ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"]; + } else { + requiredParts = ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"]; + } + break; } requiredParts.forEach(part => { const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part)); @@ -1375,7 +1414,9 @@ function handleModularSelection(category) { "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary" + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" ]; const itemType = getKey(document.getElementById("acquire-type-" + category)); @@ -1390,16 +1431,37 @@ function handleModularSelection(category) { "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary" + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" ]; - const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols"]; + const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols", "MoaPets"]; supportedModularInventoryCategory.forEach(inventoryCategory => { document.getElementById("acquire-type-" + inventoryCategory).addEventListener("input", function () { const modularFields = document.getElementById("modular-" + inventoryCategory); - if (modularWeapons.includes(getKey(this))) { - modularFields.style.display = ""; + const modularFieldsZanuka = + inventoryCategory === "MoaPets" + ? document.getElementById("modular-" + inventoryCategory + "-Zanuka") + : null; + const key = getKey(this); + + if (modularWeapons.includes(key)) { + if (key === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" && modularFieldsZanuka) { + modularFields.style.display = "none"; + modularFieldsZanuka.style.display = ""; + } else if (key === "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { + modularFields.style.display = ""; + if (modularFieldsZanuka) { + modularFieldsZanuka.style.display = "none"; + } + } else { + modularFields.style.display = ""; + } } else { modularFields.style.display = "none"; + if (modularFieldsZanuka) { + modularFieldsZanuka.style.display = "none"; + } } }); }); diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 89684cea..87d56833 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -40,6 +40,11 @@ dict = { code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`, code_succImport: `Erfolgreich importiert.`, code_gild: `Veredeln`, + code_moa: `Moa`, + code_zanuka: `Jagdhund`, + code_zanukaA: `Jagdhund: Dorma`, + code_zanukaB: `Jagdhund: Bhaira`, + code_zanukaC: `Jagdhund: Hec`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, @@ -65,6 +70,7 @@ dict = { inventory_sentinelWeapons: `Wächter-Waffen`, inventory_operatorAmps: `Verstärker`, inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 251c9ccc..6a465fc2 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -39,6 +39,11 @@ dict = { code_addModsConfirm: `Are you sure you want to add |COUNT| mods to your account?`, code_succImport: `Successfully imported.`, code_gild: `Gild`, + code_moa: `Moa`, + code_zanuka: `Hound`, + code_zanukaA: `Dorma Hound`, + code_zanukaB: `Bhaira Hound`, + code_zanukaC: `Hec Hound`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, @@ -64,6 +69,7 @@ dict = { inventory_sentinelWeapons: `Sentinel Weapons`, inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, inventory_bulkAddSuits: `Add Missing Warframes`, inventory_bulkAddWeapons: `Add Missing Weapons`, inventory_bulkAddSpaceSuits: `Add Missing Archwings`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index b9bff424..a9aafae2 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -40,6 +40,11 @@ dict = { code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, code_succImport: `Importé.`, code_gild: `[UNTRANSLATED] Gild`, + code_moa: `Moa`, + code_zanuka: `Molosse`, + code_zanukaA: `Molosse Dorma`, + code_zanukaB: `Molosse Bhaira`, + code_zanukaC: `Molosse Hec`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, @@ -65,6 +70,7 @@ dict = { inventory_sentinelWeapons: `Armes de sentinelles`, inventory_operatorAmps: `Amplificateurs`, inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, inventory_bulkAddWeapons: `Ajouter les armes manquantes`, inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 3fd2575f..a10f72e4 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -40,6 +40,11 @@ dict = { code_addModsConfirm: `Вы уверены, что хотите добавить |COUNT| модов на ваш аккаунт?`, code_succImport: `Успешно импортировано.`, code_gild: `Улучшить`, + code_moa: `МОА`, + code_zanuka: `Гончая`, + code_zanukaA: `Гончая: Дорма`, + code_zanukaB: `Гончая: Бхайра`, + code_zanukaC: `Гончая: Хек`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, @@ -65,6 +70,7 @@ dict = { inventory_sentinelWeapons: `Оружие стражей`, inventory_operatorAmps: `Усилители`, inventory_hoverboards: `К-Драйвы`, + inventory_moaPets: `МОА`, inventory_bulkAddSuits: `Добавить отсутствующие варфреймы`, inventory_bulkAddWeapons: `Добавить отсутствующее оружие`, inventory_bulkAddSpaceSuits: `Добавить отсутствующие арчвинги`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index cf23e042..b28de1cc 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -40,6 +40,11 @@ dict = { code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, code_succImport: `导入成功。`, code_gild: `[UNTRANSLATED] Gild`, + code_moa: `恐鸟`, + code_zanuka: `猎犬`, + code_zanukaA: `铎玛猎犬`, + code_zanukaB: `拜拉猎犬`, + code_zanukaC: `骸克猎犬`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, @@ -65,6 +70,7 @@ dict = { inventory_sentinelWeapons: `守护武器`, inventory_operatorAmps: `增幅器`, inventory_hoverboards: `K式悬浮板`, + inventory_moaPets: `恐鸟`, inventory_bulkAddSuits: `添加缺失战甲`, inventory_bulkAddWeapons: `添加缺失武器`, inventory_bulkAddSpaceSuits: `添加缺失Archwing`, From e2879a780877d4ed949515c1bbeb861f3f85a887 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:54:55 -0700 Subject: [PATCH 332/776] chore(webui): update translations (#1413) translations were taken from the game Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1413 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/fr.js | 2 +- static/webui/translations/zh.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index a9aafae2..09cf069c 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -39,7 +39,7 @@ dict = { code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`, code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, code_succImport: `Importé.`, - code_gild: `[UNTRANSLATED] Gild`, + code_gild: `Polir`, code_moa: `Moa`, code_zanuka: `Molosse`, code_zanukaA: `Molosse Dorma`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index b28de1cc..a99c30d1 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -39,7 +39,7 @@ dict = { code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, code_succImport: `导入成功。`, - code_gild: `[UNTRANSLATED] Gild`, + code_gild: `镀金`, code_moa: `恐鸟`, code_zanuka: `猎犬`, code_zanukaA: `铎玛猎犬`, From 7d5ea680e4bc82698803637d35266c94dee28caa Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:59:18 -0700 Subject: [PATCH 333/776] chore(webui): remove " " from item name (#1414) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1414 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/script.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/webui/script.js b/static/webui/script.js index 9538c218..eb2f6e99 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -252,6 +252,9 @@ function fetchItemList() { uniqueLevelCaps = items; } else { items.forEach(item => { + if (item.name.includes(" ")) { + item.name = item.name.replace(" ", ""); + } if ("badReason" in item) { if (item.badReason == "starter") { item.name = loc("code_starter").split("|MOD|").join(item.name); From 4a3a3de300a03f83d06d7e92b02843bcc8d4c16f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:28:52 +0200 Subject: [PATCH 334/776] chore: fix eslint warning --- src/controllers/custom/addModularEquipmentController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/custom/addModularEquipmentController.ts b/src/controllers/custom/addModularEquipmentController.ts index f1f6cd17..984acd75 100644 --- a/src/controllers/custom/addModularEquipmentController.ts +++ b/src/controllers/custom/addModularEquipmentController.ts @@ -64,7 +64,7 @@ export const addModularEquipmentController: RequestHandler = async (req, res) => "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS" ] }; - const defaultWeapons = defaultWeaponsMap[request.ItemType]; + const defaultWeapons = defaultWeaponsMap[request.ItemType] as string[] | undefined; if (defaultWeapons) { for (const defaultWeapon of defaultWeapons) { const category = ExportWeapons[defaultWeapon].productCategory; From 1a4ad8b7a5c802ef04b0e74aa9845ae14a40b392 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:28:24 -0700 Subject: [PATCH 335/776] feat: clan applications (#1410) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1410 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/addToGuildController.ts | 151 ++++++++++-------- .../api/confirmGuildInvitationController.ts | 69 +++++++- src/controllers/api/createGuildController.ts | 3 + .../api/getGuildContributionsController.ts | 5 +- .../api/removeFromGuildController.ts | 21 +++ src/models/guildModel.ts | 3 + src/routes/api.ts | 1 + src/services/guildService.ts | 4 +- src/types/guildTypes.ts | 26 ++- 9 files changed, 211 insertions(+), 72 deletions(-) diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index 41a6e227..ef75f551 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -3,82 +3,107 @@ import { Account } from "@/src/models/loginModel"; import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService"; import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; -import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; import { IOid } from "@/src/types/commonTypes"; import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; +import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { ExportFlavour } from "warframe-public-export-plus"; export const addToGuildController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as IAddToGuildRequest; - const account = await Account.findOne({ DisplayName: payload.UserName }); - if (!account) { - res.status(400).json("Username does not exist"); - return; - } + if ("UserName" in payload) { + // Clan recruiter sending an invite - const inventory = await getInventory(account._id.toString(), "Settings"); - // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented - if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") { - res.status(400).json("Invite restricted"); - return; - } - - const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; - const senderAccount = await getAccountForRequest(req); - if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { - res.status(400).json("Invalid permission"); - } - - if ( - await GuildMember.exists({ - accountId: account._id, - guildId: payload.GuildId.$oid - }) - ) { - res.status(400).json("User already invited to clan"); - return; - } - - await GuildMember.insertOne({ - accountId: account._id, - guildId: payload.GuildId.$oid, - status: 2 // outgoing invite - }); - - const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); - await createMessage(account._id.toString(), [ - { - sndr: getSuffixedName(senderAccount), - msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body", - arg: [ - { - Key: "clan", - Tag: guild.Name - } - ], - sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", - icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, - contextInfo: payload.GuildId.$oid, - highPriority: true, - acceptAction: "GUILD_INVITE", - declineAction: "GUILD_INVITE", - hasAccountAction: true + const account = await Account.findOne({ DisplayName: payload.UserName }); + if (!account) { + res.status(400).json("Username does not exist"); + return; } - ]); - const member: IGuildMemberClient = { - _id: { $oid: account._id.toString() }, - DisplayName: account.DisplayName, - Rank: 7, - Status: 2 - }; - await fillInInventoryDataForGuildMember(member); - res.json({ NewMember: member }); + const inventory = await getInventory(account._id.toString(), "Settings"); + // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented + if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") { + res.status(400).json("Invite restricted"); + return; + } + + const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; + const senderAccount = await getAccountForRequest(req); + if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { + res.status(400).json("Invalid permission"); + } + + if ( + await GuildMember.exists({ + accountId: account._id, + guildId: payload.GuildId.$oid + }) + ) { + res.status(400).json("User already invited to clan"); + return; + } + + await GuildMember.insertOne({ + accountId: account._id, + guildId: payload.GuildId.$oid, + status: 2 // outgoing invite + }); + + const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); + await createMessage(account._id, [ + { + sndr: getSuffixedName(senderAccount), + msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body", + arg: [ + { + Key: "clan", + Tag: guild.Name + } + ], + sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + contextInfo: payload.GuildId.$oid, + highPriority: true, + acceptAction: "GUILD_INVITE", + declineAction: "GUILD_INVITE", + hasAccountAction: true + } + ]); + + const member: IGuildMemberClient = { + _id: { $oid: account._id.toString() }, + DisplayName: account.DisplayName, + Rank: 7, + Status: 2 + }; + await fillInInventoryDataForGuildMember(member); + res.json({ NewMember: member }); + } else if ("RequestMsg" in payload) { + // Player applying to join a clan + const accountId = await getAccountIdForRequest(req); + try { + await GuildMember.insertOne({ + accountId, + guildId: payload.GuildId.$oid, + status: 1, // incoming invite + RequestMsg: payload.RequestMsg, + RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable. + }); + } catch (e) { + // Assuming this is "E11000 duplicate key error" due to the guildId-accountId unique index. + res.status(400).send("Already requested"); + } + res.end(); + } else { + logger.error(`data provided to ${req.path}: ${String(req.body)}`); + res.status(400).end(); + } }; interface IAddToGuildRequest { - UserName: string; + UserName?: string; GuildId: IOid; + RequestMsg?: string; } diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts index c03a4285..e65cf9db 100644 --- a/src/controllers/api/confirmGuildInvitationController.ts +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -1,18 +1,76 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Guild, GuildMember } from "@/src/models/guildModel"; -import { deleteGuild, getGuildClient, removeDojoKeyItems } from "@/src/services/guildService"; +import { Account } from "@/src/models/loginModel"; +import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService"; import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; -import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; import { Types } from "mongoose"; export const confirmGuildInvitationController: RequestHandler = async (req, res) => { + if (req.body) { + // POST request: Clan representative accepting invite(s). + const accountId = await getAccountIdForRequest(req); + const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!; + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) { + res.status(400).json("Invalid permission"); + return; + } + const payload = getJSONfromString<{ userId: string }>(String(req.body)); + const filter: { accountId?: string; status: number } = { status: 1 }; + if (payload.userId != "all") { + filter.accountId = payload.userId; + } + const guildMembers = await GuildMember.find(filter); + const newMembers: string[] = []; + for (const guildMember of guildMembers) { + guildMember.status = 0; + guildMember.RequestMsg = undefined; + guildMember.RequestExpiry = undefined; + await guildMember.save(); + + // Remove other pending applications for this account + await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 }); + + // Update inventory of new member + const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes"); + inventory.GuildId = new Types.ObjectId(req.query.clanId as string); + addRecipes(inventory, [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]); + await inventory.save(); + + // Add join to clan log + const account = (await Account.findOne({ _id: guildMember.accountId }))!; + guild.RosterActivity ??= []; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 6, + details: getSuffixedName(account) + }); + + newMembers.push(account._id.toString()); + } + await guild.save(); + res.json({ + NewMembers: newMembers + }); + return; + } + + // GET request: A player accepting an invite they got in their inbox. + const account = await getAccountForRequest(req); const invitedGuildMember = await GuildMember.findOne({ accountId: account._id, guildId: req.query.clanId as string }); - if (invitedGuildMember) { + if (invitedGuildMember && invitedGuildMember.status == 2) { let inventoryChanges: IInventoryChanges = {}; // If this account is already in a guild, we need to do cleanup first. @@ -31,6 +89,10 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res) invitedGuildMember.status = 0; await invitedGuildMember.save(); + // Remove pending applications for this account + await GuildMember.deleteMany({ accountId: account._id, status: 1 }); + + // Update inventory of new member const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); inventory.GuildId = new Types.ObjectId(req.query.clanId as string); const recipeChanges = [ @@ -45,6 +107,7 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res) const guild = (await Guild.findById(req.query.clanId as string))!; + // Add join to clan log guild.RosterActivity ??= []; guild.RosterActivity.push({ dateTime: new Date(), diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index 4d3e21c9..9b5bc768 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -9,6 +9,9 @@ export const createGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const payload = getJSONfromString(String(req.body)); + // Remove pending applications for this account + await GuildMember.deleteMany({ accountId, status: 1 }); + // Create guild on database const guild = new Guild({ Name: await createUniqueClanName(payload.guildName) diff --git a/src/controllers/api/getGuildContributionsController.ts b/src/controllers/api/getGuildContributionsController.ts index 72d61cbe..c17729f7 100644 --- a/src/controllers/api/getGuildContributionsController.ts +++ b/src/controllers/api/getGuildContributionsController.ts @@ -1,6 +1,7 @@ import { GuildMember } from "@/src/models/guildModel"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IGuildMemberClient } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const getGuildContributionsController: RequestHandler = async (req, res) => { @@ -8,11 +9,11 @@ export const getGuildContributionsController: RequestHandler = async (req, res) const guildId = (await getInventory(accountId, "GuildId")).GuildId; const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!; res.json({ - _id: { $oid: req.query.buddyId }, + _id: { $oid: req.query.buddyId as string }, RegularCreditsContributed: guildMember.RegularCreditsContributed, PremiumCreditsContributed: guildMember.PremiumCreditsContributed, MiscItemsContributed: guildMember.MiscItemsContributed, ConsumablesContributed: [], // ??? ShipDecorationsContributed: guildMember.ShipDecorationsContributed - }); + } satisfies Partial); }; diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts index 3571e1e1..db5a2ea3 100644 --- a/src/controllers/api/removeFromGuildController.ts +++ b/src/controllers/api/removeFromGuildController.ts @@ -2,6 +2,7 @@ import { GuildMember } from "@/src/models/guildModel"; import { Inbox } from "@/src/models/inboxModel"; import { Account } from "@/src/models/loginModel"; import { deleteGuild, getGuildForRequest, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService"; +import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; @@ -26,6 +27,26 @@ export const removeFromGuildController: RequestHandler = async (req, res) => { inventory.GuildId = undefined; removeDojoKeyItems(inventory); await inventory.save(); + } else if (guildMember.status == 1) { + // TOVERIFY: Is this inbox message actually sent on live? + await createMessage(guildMember.accountId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Clan/RejectedFromClan", + sub: "/Lotus/Language/Clan/RejectedFromClanHeader", + arg: [ + { + Key: "PLAYER_NAME", + Tag: (await Account.findOne({ _id: guildMember.accountId }, "DisplayName"))!.DisplayName + }, + { + Key: "CLAN_NAME", + Tag: guild.Name + } + ] + // TOVERIFY: If this message is sent on live, is it highPriority? + } + ]); } else if (guildMember.status == 2) { // Delete the inbox message for the invite await Inbox.deleteOne({ diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index cf2d1d07..0ce72882 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -218,6 +218,8 @@ const guildMemberSchema = new Schema({ guildId: Types.ObjectId, status: { type: Number, required: true }, rank: { type: Number, default: 7 }, + RequestMsg: String, + RequestExpiry: Date, RegularCreditsContributed: Number, PremiumCreditsContributed: Number, MiscItemsContributed: { type: [typeCountSchema], default: undefined }, @@ -225,6 +227,7 @@ const guildMemberSchema = new Schema({ }); guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true }); +guildMemberSchema.index({ RequestExpiry: 1 }, { expireAfterSeconds: 0 }); export const GuildMember = model("GuildMember", guildMemberSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index 970bc503..2a96f768 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -188,6 +188,7 @@ apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); +apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/contributeToVault.php", contributeToVaultController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 658b0f27..1da4a370 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -57,7 +57,9 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s const member: IGuildMemberClient = { _id: toOid(guildMember.accountId), Rank: guildMember.rank, - Status: guildMember.status + Status: guildMember.status, + Note: guildMember.RequestMsg, + RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined }; if (guildMember.accountId.equals(accountId)) { missingEntry = false; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index a5a64f87..1ff37923 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -89,19 +89,39 @@ export interface IGuildMemberDatabase { guildId: Types.ObjectId; status: number; rank: number; + RequestMsg?: string; + RequestExpiry?: Date; RegularCreditsContributed?: number; PremiumCreditsContributed?: number; MiscItemsContributed?: IMiscItem[]; ShipDecorationsContributed?: ITypeCount[]; } -export interface IGuildMemberClient { +interface IFriendInfo { _id: IOid; - Status: number; - Rank: number; DisplayName?: string; + PlatformNames?: string[]; + PlatformAccountId?: string; + Status: number; ActiveAvatarImageType?: string; + LastLogin?: IMongoDate; PlayerLevel?: number; + Suffix?: number; + Note?: string; + Favorite?: boolean; + NewRequest?: boolean; +} + +// GuildMemberInfo +export interface IGuildMemberClient extends IFriendInfo { + Rank: number; + Joined?: IMongoDate; + RequestExpiry?: IMongoDate; + RegularCreditsContributed?: number; + PremiumCreditsContributed?: number; + MiscItemsContributed?: IMiscItem[]; + ConsumablesContributed?: ITypeCount[]; + ShipDecorationsContributed?: ITypeCount[]; } export interface IGuildVault { From 404c7476422ded8d21edc2cdd0c0d9e82d2d3e1d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:28:48 -0700 Subject: [PATCH 336/776] feat: getProfileViewingData for clans (#1412) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1412 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../getProfileViewingDataController.ts | 320 ++++++++++++------ src/types/statTypes.ts | 8 + 2 files changed, 230 insertions(+), 98 deletions(-) diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts index b02189d0..c1134604 100644 --- a/src/controllers/dynamic/getProfileViewingDataController.ts +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -1,5 +1,5 @@ import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; -import { Guild } from "@/src/models/guildModel"; +import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Account } from "@/src/models/loginModel"; @@ -19,99 +19,155 @@ import { } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; import { catBreadHash } from "../api/inventoryController"; -import { ExportCustoms } from "warframe-public-export-plus"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; +import { IStatsClient } from "@/src/types/statTypes"; +import { toStoreItem } from "@/src/services/itemDataService"; export const getProfileViewingDataController: RequestHandler = async (req, res) => { - if (!req.query.playerId) { - res.status(400).end(); - return; - } - const account = await Account.findById(req.query.playerId as string, "DisplayName"); - if (!account) { - res.status(400).send("No account or guild ID specified"); - return; - } - const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; - const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + if (req.query.playerId) { + const account = await Account.findById(req.query.playerId as string, "DisplayName"); + if (!account) { + res.status(409).send("Could not find requested account"); + return; + } + const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; - const result: IPlayerProfileViewingDataResult = { - AccountId: toOid(account._id), - DisplayName: account.DisplayName, - PlayerLevel: inventory.PlayerLevel, - LoadOutInventory: { - WeaponSkins: [], - XPInfo: inventory.XPInfo - }, - PlayerSkills: inventory.PlayerSkills, - ChallengeProgress: inventory.ChallengeProgress, - DeathMarks: inventory.DeathMarks, - Harvestable: inventory.Harvestable, - DeathSquadable: inventory.DeathSquadable, - Created: toMongoDate(inventory.Created), - MigratedToConsole: false, - Missions: inventory.Missions, - Affiliations: inventory.Affiliations, - DailyFocus: inventory.DailyFocus, - Wishlist: inventory.Wishlist, - Alignment: inventory.Alignment - }; - if (inventory.CurrentLoadOutIds.length) { - result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON(); - result.LoadOutPreset.ItemId = undefined; - const skins = new Set(); - if (result.LoadOutPreset.s) { - result.LoadOutInventory.Suits = [ - inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account._id), + DisplayName: account.DisplayName, + PlayerLevel: inventory.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory.XPInfo + }, + PlayerSkills: inventory.PlayerSkills, + ChallengeProgress: inventory.ChallengeProgress, + DeathMarks: inventory.DeathMarks, + Harvestable: inventory.Harvestable, + DeathSquadable: inventory.DeathSquadable, + Created: toMongoDate(inventory.Created), + MigratedToConsole: false, + Missions: inventory.Missions, + Affiliations: inventory.Affiliations, + DailyFocus: inventory.DailyFocus, + Wishlist: inventory.Wishlist, + Alignment: inventory.Alignment + }; + await populateLoadout(inventory, result); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!; + populateGuild(guild, result); } - if (result.LoadOutPreset.p) { - result.LoadOutInventory.Pistols = [ - inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + for (const key of allDailyAffiliationKeys) { + result[key] = inventory[key]; } - if (result.LoadOutPreset.l) { - result.LoadOutInventory.LongGuns = [ - inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); - } - if (result.LoadOutPreset.m) { - result.LoadOutInventory.Melee = [ - inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]); - } - for (const skin of skins) { - result.LoadOutInventory.WeaponSkins.push({ ItemType: skin }); - } - } - if (inventory.GuildId) { - const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class"))!; - result.GuildId = toOid(inventory.GuildId); - result.GuildName = guild.Name; - result.GuildTier = guild.Tier; - result.GuildXp = guild.XP; - result.GuildClass = guild.Class; - result.GuildEmblem = false; - } - for (const key of allDailyAffiliationKeys) { - result[key] = inventory[key]; - } - const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); - delete stats._id; - delete stats.__v; - delete stats.accountOwnerId; + const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); + delete stats._id; + delete stats.__v; + delete stats.accountOwnerId; - res.json({ - Results: [result], - TechProjects: [], - XpComponents: [], - //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for - Stats: stats - }); + res.json({ + Results: [result], + TechProjects: [], + XpComponents: [], + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: stats + }); + } else if (req.query.guildId) { + const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP"); + if (!guild) { + res.status(409).send("Could not find guild"); + return; + } + const members = await GuildMember.find({ guildId: guild._id, status: 0 }); + const results: IPlayerProfileViewingDataResult[] = []; + for (let i = 0; i != Math.min(4, members.length); ++i) { + const member = members[i]; + const [account, inventory] = await Promise.all([ + Account.findById(member.accountId, "DisplayName"), + Inventory.findOne( + { accountOwnerId: member.accountId }, + "DisplayName PlayerLevel XPInfo LoadOutPresets CurrentLoadOutIds WeaponSkins Suits Pistols LongGuns Melee" + ) + ]); + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account!._id), + DisplayName: account!.DisplayName, + PlayerLevel: inventory!.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory!.XPInfo + } + }; + await populateLoadout(inventory!, result); + results.push(result); + } + populateGuild(guild, results[0]); + + const combinedStats: IStatsClient = {}; + const statsArr = await Stats.find({ accountOwnerId: { $in: members.map(x => x.accountId) } }).lean(); // need this as POJO so Object.entries works as expected + for (const stats of statsArr) { + for (const [key, value] of Object.entries(stats)) { + if (typeof value == "number" && key != "__v") { + (combinedStats[key as keyof IStatsClient] as number | undefined) ??= 0; + (combinedStats[key as keyof IStatsClient] as number) += value; + } + } + for (const arrayName of ["Weapons", "Enemies", "Scans", "Missions", "PVP"] as const) { + if (stats[arrayName]) { + combinedStats[arrayName] ??= []; + for (const entry of stats[arrayName]) { + const combinedEntry = combinedStats[arrayName].find(x => x.type == entry.type); + if (combinedEntry) { + for (const [key, value] of Object.entries(entry)) { + if (typeof value == "number") { + (combinedEntry[key as keyof typeof combinedEntry] as unknown as + | number + | undefined) ??= 0; + (combinedEntry[key as keyof typeof combinedEntry] as unknown as number) += value; + } + } + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + combinedStats[arrayName].push(entry as any); + } + } + } + } + } + + const xpComponents: IXPComponentClient[] = []; + if (guild.ClaimedXP) { + for (const componentName of guild.ClaimedXP) { + if (componentName.endsWith(".level")) { + const [key] = Object.entries(ExportDojoRecipes.rooms).find( + ([_key, value]) => value.resultType == componentName + )!; + xpComponents.push({ + StoreTypeName: toStoreItem(key) + }); + } else { + const [key] = Object.entries(ExportDojoRecipes.decos).find( + ([_key, value]) => value.resultType == componentName + )!; + xpComponents.push({ + StoreTypeName: toStoreItem(key) + }); + } + } + } + + res.json({ + Results: results, + TechProjects: guild.TechProjects, + XpComponents: xpComponents, + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: combinedStats + }); + } else { + res.sendStatus(400); + } }; interface IPlayerProfileViewingDataResult extends Partial { @@ -133,20 +189,40 @@ interface IPlayerProfileViewingDataResult extends Partial { GuildXp?: number; GuildClass?: number; GuildEmblem?: boolean; - PlayerSkills: IPlayerSkills; - ChallengeProgress: IChallengeProgress[]; - DeathMarks: string[]; - Harvestable: boolean; - DeathSquadable: boolean; - Created: IMongoDate; - MigratedToConsole: boolean; - Missions: IMission[]; - Affiliations: IAffiliation[]; - DailyFocus: number; - Wishlist: string[]; + PlayerSkills?: IPlayerSkills; + ChallengeProgress?: IChallengeProgress[]; + DeathMarks?: string[]; + Harvestable?: boolean; + DeathSquadable?: boolean; + Created?: IMongoDate; + MigratedToConsole?: boolean; + Missions?: IMission[]; + Affiliations?: IAffiliation[]; + DailyFocus?: number; + Wishlist?: string[]; Alignment?: IAlignment; } +interface IXPComponentClient { + _id?: IOid; + StoreTypeName: string; + TypeName?: string; + PurchaseQuantity?: number; + ProductCategory?: "Recipes"; + Rarity?: "COMMON"; + RegularPrice?: number; + PremiumPrice?: number; + SellingPrice?: number; + DateAddedToManifest?: number; + PrimeSellingPrice?: number; + GuildXp?: number; + ResultPrefab?: string; + ResultDecoration?: string; + ShowInMarket?: boolean; + ShowInInventory?: boolean; + locTags?: Record; +} + let skinLookupTable: Record | undefined; const resolveAndCollectSkins = ( @@ -181,3 +257,51 @@ const resolveAndCollectSkins = ( } } }; + +const populateLoadout = async ( + inventory: TInventoryDatabaseDocument, + result: IPlayerProfileViewingDataResult +): Promise => { + if (inventory.CurrentLoadOutIds.length) { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON(); + result.LoadOutPreset.ItemId = undefined; + const skins = new Set(); + if (result.LoadOutPreset.s) { + result.LoadOutInventory.Suits = [ + inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + } + if (result.LoadOutPreset.p) { + result.LoadOutInventory.Pistols = [ + inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + } + if (result.LoadOutPreset.l) { + result.LoadOutInventory.LongGuns = [ + inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); + } + if (result.LoadOutPreset.m) { + result.LoadOutInventory.Melee = [ + inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]); + } + for (const skin of skins) { + result.LoadOutInventory.WeaponSkins.push({ ItemType: skin }); + } + } +}; + +const populateGuild = (guild: TGuildDatabaseDocument, result: IPlayerProfileViewingDataResult): void => { + result.GuildId = toOid(guild._id); + result.GuildName = guild.Name; + result.GuildTier = guild.Tier; + result.GuildXp = guild.XP; + result.GuildClass = guild.Class; + result.GuildEmblem = guild.Emblem; +}; diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts index 9907c55d..970d1be5 100644 --- a/src/types/statTypes.ts +++ b/src/types/statTypes.ts @@ -31,6 +31,14 @@ export interface IStatsClient { CaliberChicksScore?: number; OlliesCrashCourseScore?: number; DojoObstacleScore?: number; + + // not in schema + PVP?: { + suitDeaths?: number; + suitKills?: number; + weaponKills?: number; + type: string; + }[]; } export interface IStatsDatabase extends IStatsClient { From 367dd3f22d0738e40647516f82f77e3ead5c4351 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:29:05 -0700 Subject: [PATCH 337/776] feat: consign pet (#1415) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1415 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/releasePetController.ts | 23 +++++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 src/controllers/api/releasePetController.ts diff --git a/src/controllers/api/releasePetController.ts b/src/controllers/api/releasePetController.ts new file mode 100644 index 00000000..10625778 --- /dev/null +++ b/src/controllers/api/releasePetController.ts @@ -0,0 +1,23 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const releasePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "RegularCredits KubrowPets"); + const payload = getJSONfromString(String(req.body)); + + const inventoryChanges = updateCurrency(inventory, 25000, false); + + inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }]; + inventory.KubrowPets.pull({ _id: payload.petId }); + + await inventory.save(); + res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here. +}; + +interface IReleasePetRequest { + recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe"; + petId: string; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 2a96f768..9845e42b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -86,6 +86,7 @@ import { projectionManagerController } from "@/src/controllers/api/projectionMan import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; +import { releasePetController } from "@/src/controllers/api/releasePetController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; @@ -236,6 +237,7 @@ apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); +apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); From 3a26d788a27f9e21221b4304212cac2dc8f41bcf Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:29:29 -0700 Subject: [PATCH 338/776] feat: zanuka capture (#1416) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1416 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 27 +++++++++++++-- src/services/missionInventoryUpdateService.ts | 33 ++++++++++++++++++- src/types/inventoryTypes/inventoryTypes.ts | 19 +++++++++++ src/types/requestTypes.ts | 10 +++++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index ecd007be..8d298d5d 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -83,7 +83,8 @@ import { INemesisClient, IInfNode, IDiscoveredMarker, - IWeeklyMission + IWeeklyMission, + ILockedWeaponGroupDatabase } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1147,6 +1148,17 @@ const alignmentSchema = new Schema( { _id: false } ); +const lockedWeaponGroupSchema = new Schema( + { + s: Schema.Types.ObjectId, + p: Schema.Types.ObjectId, + l: Schema.Types.ObjectId, + m: Schema.Types.ObjectId, + sn: Schema.Types.ObjectId + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1488,7 +1500,9 @@ const inventorySchema = new Schema( EchoesHexConquestActiveFrameVariants: { type: [String], default: undefined }, EchoesHexConquestActiveStickers: { type: [String], default: undefined }, - BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined } + // G3 + Zanuka + BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined }, + LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); @@ -1523,6 +1537,15 @@ inventorySchema.set("toJSON", { if (inventoryDatabase.BrandedSuits) { inventoryResponse.BrandedSuits = inventoryDatabase.BrandedSuits.map(toOid); } + if (inventoryDatabase.LockedWeaponGroup) { + inventoryResponse.LockedWeaponGroup = { + s: toOid(inventoryDatabase.LockedWeaponGroup.s), + l: inventoryDatabase.LockedWeaponGroup.l ? toOid(inventoryDatabase.LockedWeaponGroup.l) : undefined, + p: inventoryDatabase.LockedWeaponGroup.p ? toOid(inventoryDatabase.LockedWeaponGroup.p) : undefined, + m: inventoryDatabase.LockedWeaponGroup.m ? toOid(inventoryDatabase.LockedWeaponGroup.m) : undefined, + sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined + }; + } } }); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index f4fe1164..8fbfd41e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -47,6 +47,7 @@ import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPe import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; +import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -95,7 +96,8 @@ export const addMissionInventoryUpdates = async ( inventoryUpdates.MissionFailed && inventoryUpdates.MissionStatus == "GS_FAILURE" && inventoryUpdates.EndOfMatchUpload && - inventoryUpdates.ObjectiveReached + inventoryUpdates.ObjectiveReached && + !inventoryUpdates.LockedWeaponGroup ) { const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; @@ -397,6 +399,35 @@ export const addMissionInventoryUpdates = async ( } break; } + case "LockedWeaponGroup": { + inventory.LockedWeaponGroup = { + s: new Types.ObjectId(value.s.$oid), + l: value.l ? new Types.ObjectId(value.l.$oid) : undefined, + p: value.p ? new Types.ObjectId(value.p.$oid) : undefined, + m: value.m ? new Types.ObjectId(value.m.$oid) : undefined, + sn: value.sn ? new Types.ObjectId(value.sn.$oid) : undefined + }; + break; + } + case "UnlockWeapons": { + inventory.LockedWeaponGroup = undefined; + break; + } + case "CurrentLoadOutIds": { + const loadout = await Loadout.findOne({ loadoutOwnerId: inventory.accountOwnerId }); + if (loadout) { + for (const [loadoutId, loadoutConfig] of Object.entries(value.LoadOuts.NORMAL)) { + const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; + const loadoutConfigDatabase: ILoadoutConfigDatabase = { + _id: new Types.ObjectId(ItemId.$oid), + ...loadoutConfigItemIdRemoved + }; + loadout.NORMAL.id(loadoutId)!.overwrite(loadoutConfigDatabase); + } + await loadout.save(); + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index b4d60f20..4eee21b6 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -45,6 +45,7 @@ export interface IInventoryDatabase | "Nemesis" | "EntratiVaultCountResetDate" | "BrandedSuits" + | "LockedWeaponGroup" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -75,6 +76,7 @@ export interface IInventoryDatabase Nemesis?: INemesisDatabase; EntratiVaultCountResetDate?: Date; BrandedSuits?: Types.ObjectId[]; + LockedWeaponGroup?: ILockedWeaponGroupDatabase; } export interface IQuestKeyDatabase { @@ -349,6 +351,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EchoesHexConquestActiveFrameVariants?: string[]; EchoesHexConquestActiveStickers?: string[]; BrandedSuits?: IOid[]; + LockedWeaponGroup?: ILockedWeaponGroupClient; } export interface IAffiliation { @@ -1149,3 +1152,19 @@ export interface ISongChallenge { Song: string; Difficulties: number[]; } + +export interface ILockedWeaponGroupClient { + s: IOid; + p?: IOid; + l?: IOid; + m?: IOid; + sn?: IOid; +} + +export interface ILockedWeaponGroupDatabase { + s: Types.ObjectId; + p?: Types.ObjectId; + l?: Types.ObjectId; + m?: Types.ObjectId; + sn?: Types.ObjectId; +} diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 25158f20..299760df 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -17,7 +17,9 @@ import { ILoreFragmentScan, IUpgradeClient, ICollectibleEntry, - IDiscoveredMarker + IDiscoveredMarker, + ILockedWeaponGroupClient, + ILoadOutPresets } from "./inventoryTypes/inventoryTypes"; export interface IAffiliationChange { @@ -108,6 +110,12 @@ export type IMissionInventoryUpdateRequest = { Count: number; }[]; DiscoveredMarkers?: IDiscoveredMarker[]; + LockedWeaponGroup?: ILockedWeaponGroupClient; // sent when captured by zanuka + UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture + IncHarvester?: boolean; // sent when recovered weapons from zanuka capture + CurrentLoadOutIds?: { + LoadOuts: ILoadOutPresets; // sent when recovered weapons from zanuka capture + }; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From 2091dabfc3fd05284028f545e953264fc6961f6d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:29:41 -0700 Subject: [PATCH 339/776] chore: use tsgo to verify types (#1417) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1417 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 27 +++++++++++++++++++++++++++ package.json | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5e4ec305..0be5183e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { + "@rxliuli/tsgo": "^2025.3.31", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "eslint": "^8", @@ -307,6 +308,32 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rxliuli/tsgo": { + "version": "2025.3.31", + "resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.3.31.tgz", + "integrity": "sha512-jEistRy/+Mu79rDv/Q8xn2yIM56WF3rfQOkwrbtivumij5HBVTfY4W3EYNL3N7rop7yg9Trew3joDohDoxQ2Ow==", + "cpu": [ + "x64", + "ia32", + "arm", + "arm64" + ], + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd" + ], + "bin": { + "tsgo": "bin.js" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", diff --git a/package.json b/package.json index f0b3c4cc..e0eb4c8f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "node --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "build": "tsc --incremental && ncp static/webui build/static/webui", - "verify": "tsc --noEmit", + "verify": "tsgo --noEmit", "lint": "eslint --ext .ts .", "lint:fix": "eslint --fix --ext .ts .", "prettier": "prettier --write .", @@ -30,6 +30,7 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { + "@rxliuli/tsgo": "^2025.3.31", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "eslint": "^8", From 9e1a5d50af7ac654145b6123e0ec5a7a503fe153 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:29:51 -0700 Subject: [PATCH 340/776] chore: slightly more faithful cutoff for valence fusion (#1418) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1418 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/nemesisController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index cf15ef37..34e6bd4a 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -27,7 +27,7 @@ export const nemesisController: RequestHandler = async (req, res) => { const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); let newDamage = Math.max(destDamage, sourceDamage) * 1.1; - if (newDamage >= 0.58) { + if (newDamage >= 0.5794998) { newDamage = 0.6; } destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff); From ea9333279b98c4f831222673d7db9eb08216f88d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:48:40 -0700 Subject: [PATCH 341/776] fix: handle CurrentLoadOutIds missing LoadOuts in missionInventoryUpdate (#1421) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1421 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 22 ++++++++++--------- src/types/requestTypes.ts | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 8fbfd41e..21f1040c 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -414,17 +414,19 @@ export const addMissionInventoryUpdates = async ( break; } case "CurrentLoadOutIds": { - const loadout = await Loadout.findOne({ loadoutOwnerId: inventory.accountOwnerId }); - if (loadout) { - for (const [loadoutId, loadoutConfig] of Object.entries(value.LoadOuts.NORMAL)) { - const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; - const loadoutConfigDatabase: ILoadoutConfigDatabase = { - _id: new Types.ObjectId(ItemId.$oid), - ...loadoutConfigItemIdRemoved - }; - loadout.NORMAL.id(loadoutId)!.overwrite(loadoutConfigDatabase); + if (value.LoadOuts) { + const loadout = await Loadout.findOne({ loadoutOwnerId: inventory.accountOwnerId }); + if (loadout) { + for (const [loadoutId, loadoutConfig] of Object.entries(value.LoadOuts.NORMAL)) { + const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; + const loadoutConfigDatabase: ILoadoutConfigDatabase = { + _id: new Types.ObjectId(ItemId.$oid), + ...loadoutConfigItemIdRemoved + }; + loadout.NORMAL.id(loadoutId)!.overwrite(loadoutConfigDatabase); + } + await loadout.save(); } - await loadout.save(); } break; } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 299760df..bba719b1 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -114,7 +114,7 @@ export type IMissionInventoryUpdateRequest = { UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture IncHarvester?: boolean; // sent when recovered weapons from zanuka capture CurrentLoadOutIds?: { - LoadOuts: ILoadOutPresets; // sent when recovered weapons from zanuka capture + LoadOuts?: ILoadOutPresets; // sent when recovered weapons from zanuka capture }; } & { [K in TEquipmentKey]?: IEquipmentClient[]; From bf67a4391d5f70dc016fd36ea3401afa0c653190 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:49:08 -0700 Subject: [PATCH 342/776] feat: eleanor weapon offerings (#1419) Need to do rotating offers for her some other time Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1419 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/helpers/pathHelper.ts | 4 + src/routes/webui.ts | 3 +- src/services/buildConfigService.ts | 3 +- src/services/configService.ts | 5 +- src/services/inventoryService.ts | 35 ++-- src/services/purchaseService.ts | 14 +- src/services/rngService.ts | 9 + src/services/serversideVendorsService.ts | 98 +++++------ src/types/vendorTypes.ts | 1 + .../InfestedLichWeaponVendorManifest.json | 157 ++++++++++++++++++ 10 files changed, 247 insertions(+), 82 deletions(-) create mode 100644 src/helpers/pathHelper.ts create mode 100644 static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json diff --git a/src/helpers/pathHelper.ts b/src/helpers/pathHelper.ts new file mode 100644 index 00000000..95621f6a --- /dev/null +++ b/src/helpers/pathHelper.ts @@ -0,0 +1,4 @@ +import path from "path"; + +export const rootDir = path.join(__dirname, "../.."); +export const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; diff --git a/src/routes/webui.ts b/src/routes/webui.ts index 48f9f2fd..2cfa14da 100644 --- a/src/routes/webui.ts +++ b/src/routes/webui.ts @@ -1,9 +1,8 @@ import express from "express"; import path from "path"; +import { repoDir, rootDir } from "@/src/helpers/pathHelper"; const webuiRouter = express.Router(); -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; // Redirect / to /webui/ webuiRouter.get("/", (_req, res) => { diff --git a/src/services/buildConfigService.ts b/src/services/buildConfigService.ts index dda9b909..007b1bed 100644 --- a/src/services/buildConfigService.ts +++ b/src/services/buildConfigService.ts @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import { repoDir } from "@/src/helpers/pathHelper"; interface IBuildConfig { version: string; @@ -13,8 +14,6 @@ export const buildConfig: IBuildConfig = { matchmakingBuildId: "" }; -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; const buildConfigPath = path.join(repoDir, "static/data/buildConfig.json"); if (fs.existsSync(buildConfigPath)) { Object.assign(buildConfig, JSON.parse(fs.readFileSync(buildConfigPath, "utf-8")) as IBuildConfig); diff --git a/src/services/configService.ts b/src/services/configService.ts index 3aaa2796..6ceaf9d0 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -1,10 +1,9 @@ -import path from "path"; import fs from "fs"; import fsPromises from "fs/promises"; +import path from "path"; +import { repoDir } from "@/src/helpers/pathHelper"; import { logger } from "@/src/utils/logger"; -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; const configPath = path.join(repoDir, "config.json"); export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6eba1872..e62e51b9 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -69,7 +69,7 @@ import { addStartingGear } from "@/src/controllers/api/giveStartingGearControlle import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; -import { getRandomElement, getRandomInt } from "./rngService"; +import { getRandomElement, getRandomInt, SRng } from "./rngService"; import { createMessage } from "./inboxService"; export const createInventory = async ( @@ -230,7 +230,8 @@ export const addItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, quantity: number = 1, - premiumPurchase: boolean = false + premiumPurchase: boolean = false, + seed?: bigint ): Promise => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { @@ -380,21 +381,31 @@ export const addItem = async ( defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; } if (weapon.maxLevelCap == 40 && typeName.indexOf("BallasSword") == -1) { + if (!seed) { + seed = BigInt(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)); + } + const rng = new SRng(seed); + const tag = rng.randomElement([ + "InnateElectricityDamage", + "InnateFreezeDamage", + "InnateHeatDamage", + "InnateImpactDamage", + "InnateMagDamage", + "InnateRadDamage", + "InnateToxinDamage" + ]); + const WeaponUpgradeValueAttenuationExponent = 2.25; + let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent); + if (value >= 0.941428) { + value = 1; + } defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod"; defaultOverwrites.UpgradeFingerprint = JSON.stringify({ compat: typeName, buffs: [ { - Tag: getRandomElement([ - "InnateElectricityDamage", - "InnateFreezeDamage", - "InnateHeatDamage", - "InnateImpactDamage", - "InnateMagDamage", - "InnateRadDamage", - "InnateToxinDamage" - ]), - Value: Math.trunc(Math.random() * 0x40000000) + Tag: tag, + Value: Math.trunc(value * 0x40000000) } ] }); diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index f2b71427..04525704 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -51,6 +51,7 @@ export const handlePurchase = async ( logger.debug("purchase request", purchaseRequest); const prePurchaseInventoryChanges: IInventoryChanges = {}; + let seed: bigint | undefined; if (purchaseRequest.PurchaseParams.Source == 7) { const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (rawManifest) { @@ -74,6 +75,9 @@ export const handlePurchase = async ( prePurchaseInventoryChanges ); } + if (offer.LocTagRandSeed !== undefined) { + seed = BigInt(offer.LocTagRandSeed); + } if (!config.noVendorPurchaseLimits && ItemId) { inventory.RecentVendorPurchases ??= []; let vendorPurchases = inventory.RecentVendorPurchases.find( @@ -136,7 +140,10 @@ export const handlePurchase = async ( const purchaseResponse = await handleStoreItemAcquisition( purchaseRequest.PurchaseParams.StoreItem, inventory, - purchaseRequest.PurchaseParams.Quantity + purchaseRequest.PurchaseParams.Quantity, + undefined, + undefined, + seed ); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); @@ -324,7 +331,8 @@ export const handleStoreItemAcquisition = async ( inventory: TInventoryDatabaseDocument, quantity: number = 1, durability: TRarity = "COMMON", - ignorePurchaseQuantity: boolean = false + ignorePurchaseQuantity: boolean = false, + seed?: bigint ): Promise => { let purchaseResponse = { InventoryChanges: {} @@ -345,7 +353,7 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true) }; + purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; break; } case "Types": diff --git a/src/services/rngService.ts b/src/services/rngService.ts index cb8f2cba..b98f7bd3 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -127,4 +127,13 @@ export class SRng { } return min; } + + randomElement(arr: T[]): T { + return arr[this.randomInt(0, arr.length - 1)]; + } + + randomFloat(): number { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645; + } } diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 37a3425c..9700e00e 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,69 +1,47 @@ +import fs from "fs"; +import path from "path"; +import { repoDir } from "@/src/helpers/pathHelper"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; +import { JSONParse } from "json-with-bigint"; -import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; -import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; -import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json"; -import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json"; -import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json"; -import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json"; -import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json"; -import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json"; -import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json"; -import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json"; -import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json"; -import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; -import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; -import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; -import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json"; -import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; -import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json"; -import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; -import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; -import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; -import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; -import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; -import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; -import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; -import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json"; -import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; -import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; -import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; -import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; -import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; +const getVendorManifestJson = (name: string): IVendorManifest => { + return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); +}; const vendorManifests: IVendorManifest[] = [ - ArchimedeanVendorManifest, - DeimosEntratiFragmentVendorProductsManifest, - DeimosFishmongerVendorManifest, - DeimosHivemindCommisionsManifestFishmonger, - DeimosHivemindCommisionsManifestPetVendor, - DeimosHivemindCommisionsManifestProspector, - DeimosHivemindCommisionsManifestTokenVendor, - DeimosHivemindCommisionsManifestWeaponsmith, - DeimosHivemindTokenVendorManifest, - DeimosPetVendorManifest, - DeimosProspectorVendorManifest, - DuviriAcrithisVendorManifest, - EntratiLabsEntratiLabsCommisionsManifest, - EntratiLabsEntratiLabVendorManifest, - GuildAdvertisementVendorManifest, // uses preprocessing - HubsIronwakeDondaVendorManifest, // uses preprocessing - HubsPerrinSequenceWeaponVendorManifest, - HubsRailjackCrewMemberVendorManifest, - MaskSalesmanManifest, - Nova1999ConquestShopManifest, - OstronFishmongerVendorManifest, - OstronPetVendorManifest, - OstronProspectorVendorManifest, - RadioLegionIntermission12VendorManifest, - SolarisDebtTokenVendorManifest, - SolarisDebtTokenVendorRepossessionsManifest, - SolarisFishmongerVendorManifest, - SolarisProspectorVendorManifest, - TeshinHardModeVendorManifest, // uses preprocessing - ZarimanCommisionsManifestArchimedean + getVendorManifestJson("ArchimedeanVendorManifest"), + getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"), + getVendorManifestJson("DeimosFishmongerVendorManifest"), + getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"), + getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"), + getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"), + getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"), + getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"), + getVendorManifestJson("DeimosHivemindTokenVendorManifest"), + getVendorManifestJson("DeimosPetVendorManifest"), + getVendorManifestJson("DeimosProspectorVendorManifest"), + getVendorManifestJson("DuviriAcrithisVendorManifest"), + getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"), + getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), + getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing + getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing + getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"), + getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), + getVendorManifestJson("InfestedLichWeaponVendorManifest"), + getVendorManifestJson("MaskSalesmanManifest"), + getVendorManifestJson("Nova1999ConquestShopManifest"), + getVendorManifestJson("OstronFishmongerVendorManifest"), + getVendorManifestJson("OstronPetVendorManifest"), + getVendorManifestJson("OstronProspectorVendorManifest"), + getVendorManifestJson("RadioLegionIntermission12VendorManifest"), + getVendorManifestJson("SolarisDebtTokenVendorManifest"), + getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"), + getVendorManifestJson("SolarisFishmongerVendorManifest"), + getVendorManifestJson("SolarisProspectorVendorManifest"), + getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing + getVendorManifestJson("ZarimanCommisionsManifestArchimedean") ]; export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index d7dbd749..0aa5b83e 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -19,6 +19,7 @@ interface IItemManifest { PurchaseQuantityLimit?: number; RotatedWeekly?: boolean; AllowMultipurchase: boolean; + LocTagRandSeed?: number | bigint; Id: IOid; } diff --git a/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json new file mode 100644 index 00000000..b3b419ba --- /dev/null +++ b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json @@ -0,0 +1,157 @@ +{ + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d84" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/LongGuns/1999InfShotgun/1999InfShotgunWeapon", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 65079176837546984, + "Id": { + "$oid": "67e9da12793a120dbbc1c193" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaCaustacyst/CodaCaustacyst", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 5687904240491804000, + "Id": { + "$oid": "67e9da12793a120dbbc1c194" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaPathocyst/CodaPathocyst", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 6177144662234093000, + "Id": { + "$oid": "67e9da12793a120dbbc1c195" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Pistols/CodaTysis", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 1988275604378227700, + "Id": { + "$oid": "67e9da12793a120dbbc1c196" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/LongGuns/CodaSynapse", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 8607452585593957000, + "Id": { + "$oid": "67e9da12793a120dbbc1c197" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaHirudo", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 8385013066220909000, + "Id": { + "$oid": "67e9da12793a120dbbc1c198" + } + } + ], + "PropertyTextHash": "77093DD05A8561A022DEC9A4B9BB4A56", + "RandomSeedType": "VRST_WEAPON", + "RequiredGoalTag": "", + "WeaponUpgradeValueAttenuationExponent": 2.25, + "Expiry": { + "$date": { + "$numberLong": "9999999999999" + } + } + } +} \ No newline at end of file From 2b451a19e6962ad8937d9c6165a1d0a5f56313d6 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 2 Apr 2025 04:42:36 -0700 Subject: [PATCH 343/776] chore: remove duplicate entries (#1424) CrewShipWeapons and CrewShipSalvagedWeapons already in equipmentFields Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1424 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 8d298d5d..a7966871 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1281,9 +1281,7 @@ const inventorySchema = new Schema( //Default RailJack CrewShipAmmo: [typeCountSchema], - CrewShipWeapons: [EquipmentSchema], CrewShipWeaponSkins: [upgradeSchema], - CrewShipSalvagedWeapons: [EquipmentSchema], CrewShipSalvagedWeaponSkins: [upgradeSchema], //RailJack Crew From 158310bda274a344cfe6e23a776c2155ab191315 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 2 Apr 2025 04:43:03 -0700 Subject: [PATCH 344/776] fix(webui): blacklist modular weapons from add missing (#1425) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1425 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/script.js | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index eb2f6e99..c5239833 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -157,6 +157,15 @@ function setLanguage(lang) { } } +const webUiModularWeapons = [ + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" +]; + let uniqueLevelCaps = {}; function fetchItemList() { window.itemListPromise = new Promise(resolve => { @@ -824,7 +833,9 @@ function addMissingEquipment(categories) { "#" + category + "-list [data-item-type='" + elm.getAttribute("data-key") + "']" ) ) { - requests.push({ ItemType: elm.getAttribute("data-key"), ItemCount: 1 }); + if (!webUiModularWeapons.includes(elm.getAttribute("data-key"))) { + requests.push({ ItemType: elm.getAttribute("data-key"), ItemCount: 1 }); + } } }); }); @@ -1413,31 +1424,15 @@ function toast(text) { } function handleModularSelection(category) { - const modularWeapons = [ - "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" - ]; const itemType = getKey(document.getElementById("acquire-type-" + category)); - if (modularWeapons.includes(itemType)) { + if (webUiModularWeapons.includes(itemType)) { doAcquireModularEquipment(category, itemType); } else { doAcquireEquipment(category); } } { - const modularWeapons = [ - "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" - ]; const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols", "MoaPets"]; supportedModularInventoryCategory.forEach(inventoryCategory => { document.getElementById("acquire-type-" + inventoryCategory).addEventListener("input", function () { @@ -1448,7 +1443,7 @@ function handleModularSelection(category) { : null; const key = getKey(this); - if (modularWeapons.includes(key)) { + if (webUiModularWeapons.includes(key)) { if (key === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" && modularFieldsZanuka) { modularFields.style.display = "none"; modularFieldsZanuka.style.display = ""; From 24ed580a972a60dc8809a544ae9eaba093f9feff Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 04:59:21 -0700 Subject: [PATCH 345/776] feat: create alliance (#1423) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1423 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/createAllianceController.ts | 50 +++++++++++++++ src/controllers/api/getAllianceController.ts | 24 ++++++- src/controllers/api/getGuildController.ts | 6 +- src/models/guildModel.ts | 27 +++++++- src/routes/api.ts | 2 + src/services/guildService.ts | 38 ++++++++++- src/types/guildTypes.ts | 64 +++++++++++++++++-- 7 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 src/controllers/api/createAllianceController.ts diff --git a/src/controllers/api/createAllianceController.ts b/src/controllers/api/createAllianceController.ts new file mode 100644 index 00000000..e3a81a24 --- /dev/null +++ b/src/controllers/api/createAllianceController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const createAllianceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!; + if (guild.AllianceId) { + res.status(400).send("Guild is already in an alliance").end(); + return; + } + const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!; + if (guildMember.rank > 1) { + res.status(400).send("Invalid permission").end(); + return; + } + const data = getJSONfromString(String(req.body)); + const alliance = new Alliance({ Name: data.allianceName }); + try { + await alliance.save(); + } catch (e) { + res.status(400).send("Alliance name already in use").end(); + return; + } + guild.AllianceId = alliance._id; + await Promise.all([ + guild.save(), + AllianceMember.insertOne({ + allianceId: alliance._id, + guildId: guild._id, + Pending: false, + Permissions: + GuildPermission.Ruler | + GuildPermission.Promoter | + GuildPermission.Recruiter | + GuildPermission.Treasurer | + GuildPermission.ChatModerator + }) + ]); + res.json(await getAllianceClient(alliance, guild)); +}; + +interface ICreateAllianceRequest { + allianceName: string; +} diff --git a/src/controllers/api/getAllianceController.ts b/src/controllers/api/getAllianceController.ts index 391dae5f..5da0966d 100644 --- a/src/controllers/api/getAllianceController.ts +++ b/src/controllers/api/getAllianceController.ts @@ -1,7 +1,25 @@ +import { Alliance, Guild } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; -const getAllianceController: RequestHandler = (_req, res) => { - res.sendStatus(200); +export const getAllianceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!; + if (guild.AllianceId) { + const alliance = (await Alliance.findById(guild.AllianceId))!; + res.json(await getAllianceClient(alliance, guild)); + return; + } + } + res.end(); }; -export { getAllianceController }; +/*interface IGetAllianceRequest { + memberCount: number; + clanLeaderName: string; + clanLeaderId: string; +}*/ diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 8a803bfe..b834c289 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -5,7 +5,7 @@ import { logger } from "@/src/utils/logger"; import { getInventory } from "@/src/services/inventoryService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; -const getGuildController: RequestHandler = async (req, res) => { +export const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { @@ -28,7 +28,5 @@ const getGuildController: RequestHandler = async (req, res) => { return; } } - res.sendStatus(200); + res.end(); }; - -export { getGuildController }; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 0ce72882..ecc01151 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -11,7 +11,9 @@ import { IGuildLogEntryRoster, IGuildLogEntryContributable, IDojoLeaderboardEntry, - IGuildAdDatabase + IGuildAdDatabase, + IAllianceDatabase, + IAllianceMemberDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -167,6 +169,7 @@ const guildSchema = new Schema( TradeTax: { type: Number, default: 0 }, Tier: { type: Number, default: 1 }, Emblem: { type: Boolean }, + AllianceId: { type: Types.ObjectId }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, @@ -246,3 +249,25 @@ guildAdSchema.index({ GuildId: 1 }, { unique: true }); guildAdSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); export const GuildAd = model("GuildAd", guildAdSchema); + +const allianceSchema = new Schema({ + Name: String, + MOTD: longMOTDSchema, + LongMOTD: longMOTDSchema, + Emblem: Boolean +}); + +allianceSchema.index({ Name: 1 }, { unique: true }); + +export const Alliance = model("Alliance", allianceSchema); + +const allianceMemberSchema = new Schema({ + allianceId: Schema.Types.ObjectId, + guildId: Schema.Types.ObjectId, + Pending: Boolean, + Permissions: Number +}); + +guildMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); + +export const AllianceMember = model("AllianceMember", allianceMemberSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index 9845e42b..979090f4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -22,6 +22,7 @@ import { confirmGuildInvitationController } from "@/src/controllers/api/confirmG import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; +import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; @@ -193,6 +194,7 @@ apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/contributeToVault.php", contributeToVaultController); +apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 1da4a370..22b42c18 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,10 +1,13 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; -import { Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { GuildPermission, + IAllianceClient, + IAllianceDatabase, + IAllianceMemberClient, IDojoClient, IDojoComponentClient, IDojoComponentDatabase, @@ -99,7 +102,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s XP: guild.XP, IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), NumContributors: guild.CeremonyContributors?.length ?? 0, - CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, + AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined }; }; @@ -549,4 +553,34 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise => { }); await GuildAd.deleteOne({ GuildId: guildId }); + + await AllianceMember.deleteMany({ guildId }); + + // TODO: If this guild was the founding guild of an alliance (ruler permission), that would need to be forcefully deleted now as well. +}; + +export const getAllianceClient = async ( + alliance: IAllianceDatabase, + guild: TGuildDatabaseDocument +): Promise => { + const allianceMembers = await AllianceMember.find({ allianceId: alliance._id }); + const clans: IAllianceMemberClient[] = []; + for (const allianceMember of allianceMembers) { + const memberGuild = allianceMember.guildId.equals(guild._id) + ? guild + : (await Guild.findById(allianceMember.guildId))!; + clans.push({ + _id: toOid(allianceMember.guildId), + Name: memberGuild.Name, + Tier: memberGuild.Tier, + Pending: allianceMember.Pending, + Permissions: allianceMember.Permissions, + MemberCount: await GuildMember.countDocuments({ guildId: memberGuild._id, status: 0 }) + }); + } + return { + _id: toOid(alliance._id), + Name: alliance.Name, + Clans: clans + }; }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 1ff37923..a4b706fa 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -18,6 +18,9 @@ export interface IGuildClient { IsContributor: boolean; NumContributors: number; CeremonyResetDate?: IMongoDate; + CrossPlatformEnabled?: boolean; + AutoContributeFromVault?: boolean; + AllianceId?: IOid; } export interface IGuildDatabase { @@ -29,6 +32,7 @@ export interface IGuildDatabase { TradeTax: number; Tier: number; Emblem?: boolean; + AllianceId?: Types.ObjectId; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; @@ -60,21 +64,21 @@ export interface IGuildDatabase { export interface ILongMOTD { message: string; authorName: string; - //authorGuildName: ""; + authorGuildName?: ""; } // 32 seems to be reserved export enum GuildPermission { - Ruler = 1, // Change clan hierarchy + Ruler = 1, // Clan: Change hierarchy. Alliance: Kick clans. Advertiser = 8192, - Recruiter = 2, // Invite members + Recruiter = 2, // Send invites (Clans & Alliances) Regulator = 4, // Kick members - Promoter = 8, // Promote and demote members + Promoter = 8, // Clan: Promote and demote members. Alliance: Change clan permissions. Architect = 16, // Create and destroy rooms Decorator = 1024, // Create and destroy decos - Treasurer = 64, // Contribute from vault and edit tax rate + Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault. Tech = 128, // Queue research - ChatModerator = 512, + ChatModerator = 512, // (Clans & Alliances) Herald = 2048, // Change MOTD Fabricator = 4096 // Replicate research } @@ -268,3 +272,51 @@ export interface IGuildAdDatabase { RecruitMsg: string; Tier: number; } + +export interface IAllianceClient { + _id: IOid; + Name: string; + MOTD?: ILongMOTD; + LongMOTD?: ILongMOTD; + Emblem?: boolean; + CrossPlatformEnabled?: boolean; + Clans: IAllianceMemberClient[]; + OriginalPlatform?: number; +} + +export interface IAllianceDatabase { + _id: Types.ObjectId; + Name: string; + MOTD?: ILongMOTD; + LongMOTD?: ILongMOTD; + Emblem?: boolean; +} + +export interface IAllianceMemberClient { + _id: IOid; + Name: string; + Tier: number; + Pending: boolean; + Emblem?: boolean; + Permissions: number; + MemberCount: number; + ClanLeader?: string; + ClanLeaderId?: IOid; + OriginalPlatform?: number; +} + +export interface IAllianceMemberDatabase { + allianceId: Types.ObjectId; + guildId: Types.ObjectId; + Pending: boolean; + Permissions: number; +} + +// TODO: Alliance chat permissions +// TODO: POST /api/addToAlliance.php: {"clanName":"abc"} +// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=1 +// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 +// TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 +// TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 +// TODO: Handle alliance in contributeToVault +// TODO: Handle alliance in setGuildMotd From c55aa8a0e1b14e4b8ce54a1413b18aab7a4318c4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:51:15 +0200 Subject: [PATCH 346/776] chore: fix misplaced index --- src/models/guildModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index ecc01151..c2286f91 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -268,6 +268,6 @@ const allianceMemberSchema = new Schema({ Permissions: Number }); -guildMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); +allianceMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); export const AllianceMember = model("AllianceMember", allianceMemberSchema); From dd7805cfb28963b6e9a63941d9078de9e602c57e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:52:13 -0700 Subject: [PATCH 347/776] chore: fix some minor issues with ability infusions (#1426) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1426 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/upgradesController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index 157385f9..ae547674 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -11,7 +11,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getRecipeByResult } from "@/src/services/itemDataService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { addInfestedFoundryXP } from "./infestedFoundryController"; +import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "./infestedFoundryController"; import { config } from "@/src/services/configService"; export const upgradesController: RequestHandler = async (req, res) => { @@ -25,7 +25,7 @@ export const upgradesController: RequestHandler = async (req, res) => { operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" ) { updateCurrency(inventory, 10, true); - } else { + } else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") { addMiscItems(inventory, [ { ItemType: operation.UpgradeRequirement, @@ -66,6 +66,7 @@ export const upgradesController: RequestHandler = async (req, res) => { inventoryChanges.Recipes = recipeChanges; inventoryChanges.InfestedFoundry = inventory.toJSON().InfestedFoundry; + applyCheatsToInfestedFoundry(inventoryChanges.InfestedFoundry!); } else switch (operation.UpgradeRequirement) { case "/Lotus/Types/Items/MiscItems/OrokinReactor": From 74d9428a66e462f91b8c67f12ce6520b54823de8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:52:24 -0700 Subject: [PATCH 348/776] fix(webui): ignore MiscItems that don't have a name (#1429) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1429 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 5b3ee13b..d84d4aea 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -115,6 +115,7 @@ const getItemListsController: RequestHandler = (req, response) => { } } if ( + name && uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" && uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle" ) { From 2173bdb8b8ee08b478cd082438230dcb1a627719 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:20:06 -0700 Subject: [PATCH 349/776] fix: clamp ItemCount within 32-bit integer range (#1432) It seems the game uses 32-bit ints for these values on the C++ side before passing them on to Lua where they become floats. This would cause the game to have overflow/underflow semantics when receiving values outside of these bounds. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1432 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a7966871..01c4dbc0 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -102,6 +102,16 @@ import { EquipmentSelectionSchema } from "./loadoutModel"; export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); +typeCountSchema.set("toJSON", { + transform(_doc, obj) { + if (obj.ItemCount > 2147483647) { + obj.ItemCount = 2147483647; + } else if (obj.ItemCount < -2147483648) { + obj.ItemCount = -2147483648; + } + } +}); + const focusXPSchema = new Schema( { AP_POWER: Number, From 6dc54ed893f92947f95af03414d023cf66c2a6ae Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:26:44 -0700 Subject: [PATCH 350/776] feat: donate credits to alliance vault (#1436) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1436 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/contributeToVaultController.ts | 23 ++++++++++++++++--- src/models/guildModel.ts | 3 ++- src/services/guildService.ts | 5 +++- src/types/guildTypes.ts | 3 ++- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index 7960c951..fc03e2ca 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,4 +1,4 @@ -import { GuildMember } from "@/src/models/guildModel"; +import { Alliance, GuildMember } from "@/src/models/guildModel"; import { addGuildMemberMiscItemContribution, addVaultMiscItems, @@ -19,12 +19,27 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures"); const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; + + if (request.Alliance) { + const alliance = (await Alliance.findById(guild.AllianceId!))!; + alliance.VaultRegularCredits ??= 0; + alliance.VaultRegularCredits += request.RegularCredits; + if (request.FromVault) { + guild.VaultRegularCredits! -= request.RegularCredits; + await Promise.all([guild.save(), alliance.save()]); + } else { + updateCurrency(inventory, request.RegularCredits, false); + await Promise.all([inventory.save(), alliance.save()]); + } + res.end(); + return; + } + const guildMember = (await GuildMember.findOne( { accountId, guildId: guild._id }, "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed" ))!; - const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; - if (request.RegularCredits) { updateCurrency(inventory, request.RegularCredits, false); @@ -69,4 +84,6 @@ interface IContributeToVaultRequest { MiscItems: IMiscItem[]; ShipDecorations: ITypeCount[]; FusionTreasures: IFusionTreasure[]; + Alliance?: boolean; + FromVault?: boolean; } diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index c2286f91..70f94256 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -254,7 +254,8 @@ const allianceSchema = new Schema({ Name: String, MOTD: longMOTDSchema, LongMOTD: longMOTDSchema, - Emblem: Boolean + Emblem: Boolean, + VaultRegularCredits: Number }); allianceSchema.index({ Name: 1 }, { unique: true }); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 22b42c18..b622987e 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -581,6 +581,9 @@ export const getAllianceClient = async ( return { _id: toOid(alliance._id), Name: alliance.Name, - Clans: clans + Clans: clans, + AllianceVault: { + DojoRefundRegularCredits: alliance.VaultRegularCredits + } }; }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index a4b706fa..9361df80 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -282,6 +282,7 @@ export interface IAllianceClient { CrossPlatformEnabled?: boolean; Clans: IAllianceMemberClient[]; OriginalPlatform?: number; + AllianceVault?: IGuildVault; } export interface IAllianceDatabase { @@ -290,6 +291,7 @@ export interface IAllianceDatabase { MOTD?: ILongMOTD; LongMOTD?: ILongMOTD; Emblem?: boolean; + VaultRegularCredits?: number; } export interface IAllianceMemberClient { @@ -318,5 +320,4 @@ export interface IAllianceMemberDatabase { // TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 // TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 // TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 -// TODO: Handle alliance in contributeToVault // TODO: Handle alliance in setGuildMotd From 1b7b5a28bcb167b452df567e50f0dac7a5a7166c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 22:33:35 +0200 Subject: [PATCH 351/776] chore: limit number of kubrow eggs that can be acquired at once --- src/services/inventoryService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index e62e51b9..9f9e7327 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -308,8 +308,8 @@ export const addItem = async ( }; } else if (ExportResources[typeName].productCategory == "KubrowPetEggs") { const changes: IKubrowPetEggClient[] = []; - if (quantity < 0) { - throw new Error(`removal of KubrowPetEggs not handled`); + if (quantity < 0 || quantity > 100) { + throw new Error(`unexpected acquisition quantity of KubrowPetEggs: ${quantity}`); } for (let i = 0; i != quantity; ++i) { const egg: IKubrowPetEggDatabase = { From d4d887a5a40582a330abee9e2c4a29502cf22340 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:34:26 +0200 Subject: [PATCH 352/776] chore: prettier --- .../getVendorInfo/InfestedLichWeaponVendorManifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json index b3b419ba..04a0392d 100644 --- a/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json @@ -154,4 +154,4 @@ } } } -} \ No newline at end of file +} From cfa9ec775e0a40af06517d444e7b4ad878e3e032 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 06:17:11 -0700 Subject: [PATCH 353/776] feat: handle creditsFee in missionInventoryUpdate (#1431) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1431 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 +++++++ src/types/requestTypes.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 21f1040c..97a04fe8 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -28,6 +28,7 @@ import { addRecipes, addShipDecorations, combineInventoryChanges, + updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; @@ -430,6 +431,12 @@ export const addMissionInventoryUpdates = async ( } break; } + case "creditsFee": { + updateCurrency(inventory, value, false); + inventoryChanges.RegularCredits ??= 0; + inventoryChanges.RegularCredits -= value; + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index bba719b1..7c14f696 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -116,6 +116,8 @@ export type IMissionInventoryUpdateRequest = { CurrentLoadOutIds?: { LoadOuts?: ILoadOutPresets; // sent when recovered weapons from zanuka capture }; + wagerTier?: number; // the index + creditsFee?: number; // the index } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From 92cf85084f9accb617743ff80900f21639a2ee8f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 06:17:38 -0700 Subject: [PATCH 354/776] chore: remove needless query when sending clan invite (#1434) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1434 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/addToGuildController.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index ef75f551..d2a89df4 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -35,22 +35,18 @@ export const addToGuildController: RequestHandler = async (req, res) => { res.status(400).json("Invalid permission"); } - if ( - await GuildMember.exists({ + try { + await GuildMember.insertOne({ accountId: account._id, - guildId: payload.GuildId.$oid - }) - ) { + guildId: payload.GuildId.$oid, + status: 2 // outgoing invite + }); + } catch (e) { + logger.debug(`guild invite failed due to ${String(e)}`); res.status(400).json("User already invited to clan"); return; } - await GuildMember.insertOne({ - accountId: account._id, - guildId: payload.GuildId.$oid, - status: 2 // outgoing invite - }); - const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); await createMessage(account._id, [ { @@ -92,7 +88,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable. }); } catch (e) { - // Assuming this is "E11000 duplicate key error" due to the guildId-accountId unique index. + logger.debug(`alliance invite failed due to ${String(e)}`); res.status(400).send("Already requested"); } res.end(); From 05c0c9909cec20af6866fcf43e73caf1e2735f96 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:37:52 -0700 Subject: [PATCH 355/776] fix: ignore purchaseQuantity when giving mission rewards (#1446) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1446 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 97a04fe8..0ff1e1d4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -537,7 +537,13 @@ export const addMissionRewards = async ( } for (const reward of MissionRewards) { - const inventoryChange = await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount); + const inventoryChange = await handleStoreItemAcquisition( + reward.StoreItem, + inventory, + reward.ItemCount, + undefined, + true + ); //TODO: combineInventoryChanges improve type safety, merging 2 of the same item? //TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them, but the client also merges them //TODO: some conditional types to rule out binchanges? From d918b0c982054a583d1538641609477e93659bc4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:38:11 -0700 Subject: [PATCH 356/776] fix: don't remove consumed argon crystals from FoundToday (#1447) This fixes a possible mongo conflict when ticking them, and this is probably more desirable as you wanna consume unstable crystals first. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1447 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 | 6 ++++-- src/services/inventoryService.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index e2f4a99d..cef69be7 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -51,9 +51,11 @@ export const inventoryController: RequestHandler = async (request, response) => if (numArgonCrystals == 0) { break; } - const numStableArgonCrystals = + const numStableArgonCrystals = Math.min( + numArgonCrystals, inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") - ?.ItemCount ?? 0; + ?.ItemCount ?? 0 + ); const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals; const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2); logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, { diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9f9e7327..b15bde8c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1119,7 +1119,7 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: MiscItems[itemIndex].ItemCount += ItemCount; - if (ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") { + if (ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal" && ItemCount > 0) { inventory.FoundToday ??= []; let foundTodayIndex = inventory.FoundToday.findIndex(x => x.ItemType == ItemType); if (foundTodayIndex == -1) { From 0c2f72f9b1a16eff1cbf26cd9f0d0de28611edd2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:38:23 -0700 Subject: [PATCH 357/776] fix: don't charge platinum for renaming kaithe (#1440) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1440 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/nameWeaponController.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/nameWeaponController.ts b/src/controllers/api/nameWeaponController.ts index f4fc88a7..5d1011be 100644 --- a/src/controllers/api/nameWeaponController.ts +++ b/src/controllers/api/nameWeaponController.ts @@ -18,7 +18,11 @@ export const nameWeaponController: RequestHandler = async (req, res) => { } else { item.ItemName = undefined; } - const currencyChanges = updateCurrency(inventory, "webui" in req.query ? 0 : 15, true); + const currencyChanges = updateCurrency( + inventory, + req.query.Category == "Horses" || "webui" in req.query ? 0 : 15, + true + ); await inventory.save(); res.json({ InventoryChanges: currencyChanges From ed10a89c1d7644d54ef65f40846aaf3445b19914 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:38:37 -0700 Subject: [PATCH 358/776] feat: alliance motd (#1438) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1438 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/setGuildMotdController.ts | 58 +++++++++++++------ src/models/guildModel.ts | 3 +- src/services/guildService.ts | 2 + src/types/guildTypes.ts | 3 +- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts index 374ae0f1..8f1e28a7 100644 --- a/src/controllers/api/setGuildMotdController.ts +++ b/src/controllers/api/setGuildMotdController.ts @@ -1,35 +1,59 @@ -import { Guild } from "@/src/models/guildModel"; -import { hasGuildPermission } from "@/src/services/guildService"; +import { Alliance, Guild, GuildMember } from "@/src/models/guildModel"; +import { hasGuildPermissionEx } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; -import { GuildPermission } from "@/src/types/guildTypes"; +import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; export const setGuildMotdController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); const inventory = await getInventory(account._id.toString(), "GuildId"); const guild = (await Guild.findById(inventory.GuildId!))!; - if (!(await hasGuildPermission(guild, account._id, GuildPermission.Herald))) { - res.status(400).json("Invalid permission"); - return; - } + const member = (await GuildMember.findOne({ accountId: account._id, guildId: guild._id }))!; const IsLongMOTD = "longMOTD" in req.query; const MOTD = req.body ? String(req.body) : undefined; - if (IsLongMOTD) { - if (MOTD) { - guild.LongMOTD = { - message: MOTD, - authorName: getSuffixedName(account) - }; - } else { - guild.LongMOTD = undefined; + if ("alliance" in req.query) { + if (member.rank > 1) { + res.status(400).json("Invalid permission"); + return; } + + const alliance = (await Alliance.findById(guild.AllianceId!))!; + const motd = MOTD + ? ({ + message: MOTD, + authorName: getSuffixedName(account), + authorGuildName: guild.Name + } satisfies ILongMOTD) + : undefined; + if (IsLongMOTD) { + alliance.LongMOTD = motd; + } else { + alliance.MOTD = motd; + } + await alliance.save(); } else { - guild.MOTD = MOTD ?? ""; + if (!hasGuildPermissionEx(guild, member, GuildPermission.Herald)) { + res.status(400).json("Invalid permission"); + return; + } + + if (IsLongMOTD) { + if (MOTD) { + guild.LongMOTD = { + message: MOTD, + authorName: getSuffixedName(account) + }; + } else { + guild.LongMOTD = undefined; + } + } else { + guild.MOTD = MOTD ?? ""; + } + await guild.save(); } - await guild.save(); res.json({ IsLongMOTD, MOTD }); }; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 70f94256..cd173522 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -71,7 +71,8 @@ const techProjectSchema = new Schema( const longMOTDSchema = new Schema( { message: String, - authorName: String + authorName: String, + authorGuildName: String }, { _id: false } ); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index b622987e..bb646b46 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -581,6 +581,8 @@ export const getAllianceClient = async ( return { _id: toOid(alliance._id), Name: alliance.Name, + MOTD: alliance.MOTD, + LongMOTD: alliance.LongMOTD, Clans: clans, AllianceVault: { DojoRefundRegularCredits: alliance.VaultRegularCredits diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 9361df80..5ec147d0 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -64,7 +64,7 @@ export interface IGuildDatabase { export interface ILongMOTD { message: string; authorName: string; - authorGuildName?: ""; + authorGuildName?: string; } // 32 seems to be reserved @@ -320,4 +320,3 @@ export interface IAllianceMemberDatabase { // TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 // TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 // TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 -// TODO: Handle alliance in setGuildMotd From 9eadc7fa21567498a7456e9504a16cb23c577307 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:39:16 -0700 Subject: [PATCH 359/776] feat: auto-contribute from clan vault (#1435) The wiki says this is also supposed to do partial fills, but didn't see that in my testing on live. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1435 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/placeDecoInComponentController.ts | 40 ++++++++++++++++++- .../api/saveVaultAutoContributeController.ts | 25 ++++++++++++ src/models/guildModel.ts | 1 + src/routes/api.ts | 2 + src/types/guildTypes.ts | 1 + 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/saveVaultAutoContributeController.ts diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index 08f814da..3a09ddfc 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -1,4 +1,12 @@ -import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { + getDojoClient, + getGuildForRequestEx, + getVaultMiscItemCount, + hasAccessToDojo, + hasGuildPermission, + processDojoBuildMaterialsGathered, + scaleRequiredCount +} from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; @@ -42,6 +50,36 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = } if (meta.price == 0 && meta.ingredients.length == 0) { deco.CompletionTime = new Date(); + } else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { + if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { + let enoughMiscItems = true; + for (const ingredient of meta.ingredients) { + if ( + getVaultMiscItemCount(guild, ingredient.ItemType) < + scaleRequiredCount(guild.Tier, ingredient.ItemCount) + ) { + enoughMiscItems = false; + break; + } + } + if (enoughMiscItems) { + guild.VaultRegularCredits -= meta.price; + deco.RegularCredits = meta.price; + + deco.MiscItems = []; + for (const ingredient of meta.ingredients) { + guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -= + scaleRequiredCount(guild.Tier, ingredient.ItemCount); + deco.MiscItems.push({ + ItemType: ingredient.ItemType, + ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount) + }); + } + + deco.CompletionTime = new Date(Date.now() + meta.time * 1000); + processDojoBuildMaterialsGathered(guild, meta); + } + } } } diff --git a/src/controllers/api/saveVaultAutoContributeController.ts b/src/controllers/api/saveVaultAutoContributeController.ts new file mode 100644 index 00000000..5c5f51c5 --- /dev/null +++ b/src/controllers/api/saveVaultAutoContributeController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const saveVaultAutoContributeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = (await Guild.findById(inventory.GuildId!, "Ranks AutoContributeFromVault"))!; + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) { + res.status(400).send("Invalid permission").end(); + return; + } + const data = getJSONfromString(String(req.body)); + guild.AutoContributeFromVault = data.autoContributeFromVault; + await guild.save(); + res.end(); +}; + +interface ISetVaultAutoContributeRequest { + autoContributeFromVault: boolean; +} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index cd173522..cce21847 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -170,6 +170,7 @@ const guildSchema = new Schema( TradeTax: { type: Number, default: 0 }, Tier: { type: Number, default: 1 }, Emblem: { type: Boolean }, + AutoContributeFromVault: { type: Boolean }, AllianceId: { type: Types.ObjectId }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, diff --git a/src/routes/api.ts b/src/routes/api.ts index 979090f4..cadce506 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -94,6 +94,7 @@ import { retrievePetFromStasisController } from "@/src/controllers/api/retrieveP import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadout"; import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; +import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController"; import { sellController } from "@/src/controllers/api/sellController"; import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController"; import { setActiveShipController } from "@/src/controllers/api/setActiveShipController"; @@ -246,6 +247,7 @@ apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveLoadout.php", saveLoadoutController); apiRouter.post("/saveSettings.php", saveSettingsController); +apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController); apiRouter.post("/sell.php", sellController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 5ec147d0..21cb16c6 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -32,6 +32,7 @@ export interface IGuildDatabase { TradeTax: number; Tier: number; Emblem?: boolean; + AutoContributeFromVault?: boolean; AllianceId?: Types.ObjectId; DojoComponents: IDojoComponentDatabase[]; From 5cc991baca71c0e16a1108cc4569b0e6e46d59b8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:40:02 -0700 Subject: [PATCH 360/776] fix: reduce DailyFocus by earned focus XP (#1448) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1448 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index b15bde8c..3e4835da 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1240,6 +1240,8 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; + + inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); }; export const addSeasonalChallengeHistory = ( From 710470ca2dfab54f5a6ff696f9c3ca8082666da7 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:40:22 -0700 Subject: [PATCH 361/776] feat(webui): quests support (#1411) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1411 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- Dockerfile | 2 - .../custom/getItemListsController.ts | 12 ++ .../custom/manageQuestsController.ts | 191 +++++++++++------- src/routes/webui.ts | 3 + src/services/questService.ts | 152 +++++++++----- static/webui/index.html | 51 ++--- static/webui/script.js | 137 ++++++++++++- static/webui/translations/de.js | 19 +- static/webui/translations/en.js | 19 +- static/webui/translations/fr.js | 19 +- static/webui/translations/ru.js | 19 +- static/webui/translations/zh.js | 19 +- 12 files changed, 460 insertions(+), 183 deletions(-) diff --git a/Dockerfile b/Dockerfile index f265957f..8913def2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,6 @@ ENV APP_SKIP_TUTORIAL=true ENV APP_SKIP_ALL_DIALOGUE=true ENV APP_UNLOCK_ALL_SCANS=true ENV APP_UNLOCK_ALL_MISSIONS=true -ENV APP_UNLOCK_ALL_QUESTS=true -ENV APP_COMPLETE_ALL_QUESTS=true ENV APP_INFINITE_RESOURCES=true ENV APP_UNLOCK_ALL_SHIP_FEATURES=true ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index d84d4aea..7d60f896 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -5,6 +5,7 @@ import { ExportAvionics, ExportDrones, ExportGear, + ExportKeys, ExportMisc, ExportRailjackWeapons, ExportRecipes, @@ -26,6 +27,7 @@ interface ListedItem { exalted?: string[]; badReason?: "starter" | "frivolous" | "notraw"; partType?: string; + chainLength?: number; } const relicQualitySuffixes: Record = { @@ -52,6 +54,7 @@ const getItemListsController: RequestHandler = (req, response) => { res.miscitems = []; res.Syndicates = []; res.OperatorAmps = []; + res.QuestKeys = []; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ uniqueName, @@ -208,6 +211,15 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(syndicate.name, lang) }); } + for (const [uniqueName, key] of Object.entries(ExportKeys)) { + if (key.chainStages) { + res.QuestKeys.push({ + uniqueName, + name: getString(key.name || "", lang), + chainLength: key.chainStages.length + }); + } + } response.json({ archonCrystalUpgrades, diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 2234ec00..49ae004c 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -1,7 +1,11 @@ -import { addString } from "@/src/controllers/api/inventoryController"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addQuestKey, completeQuest, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService"; +import { + addQuestKey, + completeQuest, + giveKeyChainMissionReward, + giveKeyChainStageTriggered +} from "@/src/services/questService"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { ExportKeys } from "warframe-public-export-plus"; @@ -9,13 +13,17 @@ import { ExportKeys } from "warframe-public-export-plus"; export const manageQuestsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const operation = req.query.operation as - | "unlockAll" | "completeAll" - | "ResetAll" - | "completeAllUnlocked" - | "updateKey" - | "giveAll"; - const questKeyUpdate = req.body as IUpdateQuestRequest["QuestKeys"]; + | "resetAll" + | "giveAll" + | "completeKey" + | "deleteKey" + | "resetKey" + | "prevStage" + | "nextStage" + | "setInactive"; + + const questItemType = req.query.itemType as string; const allQuestKeys: string[] = []; for (const [k, v] of Object.entries(ExportKeys)) { @@ -26,47 +34,15 @@ export const manageQuestsController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); switch (operation) { - case "updateKey": { - //TODO: if this is intended to be used, one needs to add a updateQuestKeyMultiple, the game does never intend to do it, so it errors for multiple keys. - await updateQuestKey(inventory, questKeyUpdate); - break; - } - case "unlockAll": { - for (const questKey of allQuestKeys) { - addQuestKey(inventory, { ItemType: questKey, Completed: false, unlock: true, Progress: [] }); - } - break; - } case "completeAll": { - logger.info("completing all quests.."); - for (const questKey of allQuestKeys) { - try { - await completeQuest(inventory, questKey); - } catch (error) { - if (error instanceof Error) { - logger.error( - `Something went wrong completing quest ${questKey}, probably could not add some item` - ); - logger.error(error.message); - } - } - - //Skip "Watch The Maker" - if (questKey === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") { - addString( - inventory.NodeIntrosCompleted, - "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level" - ); - } - - if (questKey === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") { - inventory.ArchwingEnabled = true; + if (allQuestKeys.includes(questItemType)) { + for (const questKey of inventory.QuestKeys) { + await completeQuest(inventory, questKey.ItemType); } } break; } - case "ResetAll": { - logger.info("resetting all quests.."); + case "resetAll": { for (const questKey of inventory.QuestKeys) { questKey.Completed = false; questKey.Progress = []; @@ -75,40 +51,113 @@ export const manageQuestsController: RequestHandler = async (req, res) => { inventory.ActiveQuest = ""; break; } - case "completeAllUnlocked": { - logger.info("completing all unlocked quests.."); - for (const questKey of inventory.QuestKeys) { - try { + case "giveAll": { + allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey })); + break; + } + case "deleteKey": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + + inventory.QuestKeys.pull({ ItemType: questItemType }); + } + break; + } + case "completeKey": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + + await completeQuest(inventory, questItemType); + } + break; + } + case "resetKey": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + + questKey.Completed = false; + questKey.Progress = []; + questKey.CompletionDate = undefined; + } + break; + } + case "prevStage": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + if (!questKey.Progress) break; + + if (questKey.Completed) { + questKey.Completed = false; + questKey.CompletionDate = undefined; + } + questKey.Progress.pop(); + const stage = questKey.Progress.length - 1; + if (stage > 0) { + await giveKeyChainStageTriggered(inventory, { + KeyChain: questKey.ItemType, + ChainStage: stage + }); + } + } + break; + } + case "nextStage": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + const questManifest = ExportKeys[questItemType]; + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + if (!questKey.Progress) break; + + const currentStage = questKey.Progress.length; + if (currentStage + 1 == questManifest.chainStages?.length) { + logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`); await completeQuest(inventory, questKey.ItemType); - } catch (error) { - if (error instanceof Error) { - logger.error( - `Something went wrong completing quest ${questKey.ItemType}, probably could not add some item` - ); - logger.error(error.message); + } else { + const progress = { + c: questManifest.chainStages![currentStage].key ? -1 : 0, + i: false, + m: false, + b: [] + }; + questKey.Progress.push(progress); + + await giveKeyChainStageTriggered(inventory, { + KeyChain: questKey.ItemType, + ChainStage: currentStage + }); + + if (currentStage > 0) { + await giveKeyChainMissionReward(inventory, { + KeyChain: questKey.ItemType, + ChainStage: currentStage - 1 + }); } } - - //Skip "Watch The Maker" - if (questKey.ItemType === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") { - addString( - inventory.NodeIntrosCompleted, - "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level" - ); - } - - if (questKey.ItemType === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") { - inventory.ArchwingEnabled = true; - } } break; } - case "giveAll": { - for (const questKey of allQuestKeys) { - addQuestKey(inventory, { ItemType: questKey }); - } + case "setInactive": + inventory.ActiveQuest = ""; break; - } } await inventory.save(); diff --git a/src/routes/webui.ts b/src/routes/webui.ts index 2cfa14da..02224903 100644 --- a/src/routes/webui.ts +++ b/src/routes/webui.ts @@ -30,6 +30,9 @@ webuiRouter.get("/webui/mods", (_req, res) => { webuiRouter.get("/webui/settings", (_req, res) => { res.sendFile(path.join(rootDir, "static/webui/index.html")); }); +webuiRouter.get("/webui/quests", (_req, res) => { + res.sendFile(path.join(rootDir, "static/webui/index.html")); +}); webuiRouter.get("/webui/cheats", (_req, res) => { res.sendFile(path.join(rootDir, "static/webui/index.html")); }); diff --git a/src/services/questService.ts b/src/services/questService.ts index a8a20629..7b82b304 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -130,73 +130,56 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest throw new Error(`Quest ${questKey} does not contain chain stages`); } - const chainStageTotal = ExportKeys[questKey].chainStages?.length ?? 0; + const chainStageTotal = chainStages.length; const existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); + const startingStage = Math.max((existingQuestKey?.Progress?.length ?? 0) - 1, 0); + if (existingQuestKey?.Completed) { return; } - const Progress = Array(chainStageTotal).fill({ - c: 0, - i: false, - m: false, - b: [] - } satisfies IQuestStage); - - const completedQuestKey: IQuestKeyDatabase = { - ItemType: questKey, - Completed: true, - unlock: true, - Progress: Progress, - CompletionDate: new Date() - }; - - //overwrite current quest progress, might lead to multiple quest item rewards if (existingQuestKey) { - existingQuestKey.overwrite(completedQuestKey); - //Object.assign(existingQuestKey, completedQuestKey); + existingQuestKey.Progress = existingQuestKey.Progress ?? []; + + const existingProgressLength = existingQuestKey.Progress.length; + + if (existingProgressLength < chainStageTotal) { + const missingProgress: IQuestStage[] = Array.from( + { length: chainStageTotal - existingProgressLength }, + () => + ({ + c: 0, + i: false, + m: false, + b: [] + }) as IQuestStage + ); + + existingQuestKey.Progress.push(...missingProgress); + existingQuestKey.CompletionDate = new Date(); + existingQuestKey.Completed = true; + } } else { + const completedQuestKey: IQuestKeyDatabase = { + ItemType: questKey, + Completed: true, + unlock: true, + Progress: Array(chainStageTotal).fill({ + c: 0, + i: false, + m: false, + b: [] + } satisfies IQuestStage), + CompletionDate: new Date() + }; addQuestKey(inventory, completedQuestKey); } - for (let i = 0; i < chainStageTotal; i++) { - if (chainStages[i].itemsToGiveWhenTriggered.length > 0) { - await giveKeyChainItem(inventory, { KeyChain: questKey, ChainStage: i }); - } + for (let i = startingStage; i < chainStageTotal; i++) { + await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i }); - if (chainStages[i].messageToSendWhenTriggered) { - await giveKeyChainMessage(inventory, inventory.accountOwnerId, { - KeyChain: questKey, - ChainStage: i - }); - } - - const missionName = chainStages[i].key; - if (missionName) { - const fixedLevelRewards = getLevelKeyRewards(missionName); - //logger.debug(`fixedLevelRewards`, fixedLevelRewards); - if (fixedLevelRewards.levelKeyRewards) { - const missionRewards: { StoreItem: string; ItemCount: number }[] = []; - addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); - - for (const reward of missionRewards) { - await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount); - } - } else if (fixedLevelRewards.levelKeyRewards2) { - for (const reward of fixedLevelRewards.levelKeyRewards2) { - if (reward.rewardType == "RT_CREDITS") { - inventory.RegularCredits += reward.amount; - continue; - } - if (reward.rewardType == "RT_RESOURCE") { - await addItem(inventory, fromStoreItem(reward.itemType), reward.amount); - } else { - await addItem(inventory, fromStoreItem(reward.itemType)); - } - } - } - } + await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); } const questCompletionItems = getQuestCompletionItems(questKey); @@ -205,7 +188,7 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest await addItems(inventory, questCompletionItems); } - inventory.ActiveQuest = ""; + if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = ""; if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { setupKahlSyndicate(inventory); @@ -247,3 +230,60 @@ export const giveKeyChainMessage = async ( updateQuestStage(inventory, keyChainInfo, { m: true }); }; + +export const giveKeyChainMissionReward = async ( + inventory: TInventoryDatabaseDocument, + keyChainInfo: IKeyChainRequest +): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages; + + if (chainStages) { + const missionName = chainStages[keyChainInfo.ChainStage].key; + if (missionName) { + const fixedLevelRewards = getLevelKeyRewards(missionName); + if (fixedLevelRewards.levelKeyRewards) { + const missionRewards: { StoreItem: string; ItemCount: number }[] = []; + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); + + for (const reward of missionRewards) { + await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount); + } + + updateQuestStage(inventory, keyChainInfo, { c: 0 }); + } else if (fixedLevelRewards.levelKeyRewards2) { + for (const reward of fixedLevelRewards.levelKeyRewards2) { + if (reward.rewardType == "RT_CREDITS") { + inventory.RegularCredits += reward.amount; + continue; + } + if (reward.rewardType == "RT_RESOURCE") { + await addItem(inventory, fromStoreItem(reward.itemType), reward.amount); + } else { + await addItem(inventory, fromStoreItem(reward.itemType)); + } + } + + updateQuestStage(inventory, keyChainInfo, { c: 0 }); + } + } + } +}; + +export const giveKeyChainStageTriggered = async ( + inventory: TInventoryDatabaseDocument, + keyChainInfo: IKeyChainRequest +): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages; + + if (chainStages) { + if (chainStages[keyChainInfo.ChainStage].itemsToGiveWhenTriggered.length > 0) { + await giveKeyChainItem(inventory, keyChainInfo); + } + + if (chainStages[keyChainInfo.ChainStage].messageToSendWhenTriggered) { + await giveKeyChainMessage(inventory, inventory.accountOwnerId, keyChainInfo); + } + } +}; diff --git a/static/webui/index.html b/static/webui/index.html index aefb1f11..74ea23d6 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -61,6 +61,9 @@ + @@ -470,22 +473,31 @@
-
-
-
- - -
+
+
+
+
+
+
+ + +
+ + +
+
+
-
-
-
-
-
- - - - +
+
+
+
+
+ + + +
+
@@ -633,14 +645,6 @@
-
-
- - - - - -
@@ -666,6 +670,7 @@ + diff --git a/static/webui/script.js b/static/webui/script.js index c5239833..849062b2 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -489,6 +489,132 @@ function updateInventory() { }); }); + // Populate quests route + document.getElementById("QuestKeys-list").innerHTML = ""; + data.QuestKeys.forEach(item => { + const tr = document.createElement("tr"); + tr.setAttribute("data-item-type", item.ItemType); + const stage = item.Progress?.length ?? 0; + + const datalist = document.getElementById("datalist-QuestKeys"); + const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`); + if (optionToRemove) { + datalist.removeChild(optionToRemove); + } + + { + const td = document.createElement("td"); + td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType; + if (!item.Completed) { + td.textContent += + " | " + loc("code_stage") + ": [" + stage + "/" + itemMap[item.ItemType].chainLength + "]"; + } else { + td.textContent += " | " + loc("code_completed"); + } + + if (data.ActiveQuest == item.ItemType) td.textContent += " | " + loc("code_active"); + tr.appendChild(td); + } + { + const td = document.createElement("td"); + td.classList = "text-end text-nowrap"; + if (data.ActiveQuest == item.ItemType && !item.Completed) { + console.log(data.ActiveQuest); + + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("setInactive", item.ItemType); + }; + a.title = loc("code_setInactive"); + a.innerHTML = ``; + td.appendChild(a); + } + if (stage > 0) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("resetKey", item.ItemType); + }; + a.title = loc("code_reset"); + a.innerHTML = ``; + td.appendChild(a); + } + if (itemMap[item.ItemType].chainLength > stage && !item.Completed) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("completeKey", item.ItemType); + }; + a.title = loc("code_complete"); + a.innerHTML = ``; + td.appendChild(a); + } + if (stage > 0 && itemMap[item.ItemType].chainLength > 1) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("prevStage", item.ItemType); + }; + a.title = loc("code_prevStage"); + a.innerHTML = ``; + td.appendChild(a); + } + if ( + itemMap[item.ItemType].chainLength > stage && + !item.Completed && + itemMap[item.ItemType].chainLength > 1 + ) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("nextStage", item.ItemType); + }; + a.title = loc("code_nextStage"); + a.innerHTML = ``; + td.appendChild(a); + } + { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + const option = document.createElement("option"); + option.setAttribute("data-key", item.ItemType); + option.value = itemMap[item.ItemType]?.name ?? item.ItemType; + document.getElementById("datalist-QuestKeys").appendChild(option); + doQuestUpdate("deleteKey", item.ItemType); + }; + a.title = loc("code_remove"); + a.innerHTML = ``; + td.appendChild(a); + } + tr.appendChild(td); + } + document.getElementById("QuestKeys-list").appendChild(tr); + }); + + const datalistQuestKeys = document.querySelectorAll("#datalist-QuestKeys option"); + const form = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]"); + const giveAllQuestButton = document.querySelector("button[onclick*=\"doBulkQuestUpdate('giveAll')\"]"); + + if (datalistQuestKeys.length === 0) { + form.classList.add("disabled"); + form.querySelector("input").disabled = true; + form.querySelector("button").disabled = true; + giveAllQuestButton.disabled = true; + } else { + form.classList.remove("disabled"); + form.querySelector("input").disabled = false; + form.querySelector("button").disabled = false; + giveAllQuestButton.disabled = false; + } + // Populate mods route document.getElementById("riven-list").innerHTML = ""; document.getElementById("mods-list").innerHTML = ""; @@ -1397,7 +1523,16 @@ function doAddCurrency(currency) { }); } -function doQuestUpdate(operation) { +function doQuestUpdate(operation, itemType) { + $.post({ + url: "/custom/manageQuests?" + window.authz + "&operation=" + operation + "&itemType=" + itemType, + contentType: "application/json" + }).then(function () { + updateInventory(); + }); +} + +function doBulkQuestUpdate(operation) { $.post({ url: "/custom/manageQuests?" + window.authz + "&operation=" + operation, contentType: "application/json" diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 87d56833..f04d2780 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -45,6 +45,14 @@ dict = { code_zanukaA: `Jagdhund: Dorma`, code_zanukaB: `Jagdhund: Bhaira`, code_zanukaC: `Jagdhund: Hec`, + code_stage: `[UNTRANSLATED] Stage`, + code_complete: `[UNTRANSLATED] Complete`, + code_nextStage: `[UNTRANSLATED] Next stage`, + code_prevStage: `[UNTRANSLATED] Previous stage`, + code_reset: `[UNTRANSLATED] Reset`, + code_setInactive: `[UNTRANSLATED] Make the quest inactive`, + code_completed: `[UNTRANSLATED] Completed`, + code_active: `[UNTRANSLATED] Active`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, @@ -84,6 +92,11 @@ dict = { inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`, inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`, + quests_list: `Quests`, + quests_completeAll: `Alle Quests abschließen`, + quests_resetAll: `Alle Quests zurücksetzen`, + quests_giveAll: `Alle Quests erhalten`, + currency_RegularCredits: `Credits`, currency_PremiumCredits: `Platinum`, currency_FusionPoints: `Endo`, @@ -135,12 +148,6 @@ dict = { cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`, cheats_changeButton: `Ändern`, cheats_none: `Keines`, - cheats_quests: `Quests`, - cheats_quests_unlockAll: `Alle Quests freischalten`, - cheats_quests_completeAll: `Alle Quests abschließen`, - cheats_quests_completeAllUnlocked: `Alle freigeschalteten Quests abschließen`, - cheats_quests_resetAll: `Alle Quests zurücksetzen`, - cheats_quests_giveAll: `Alle Quests erhalten`, import_importNote: `Du kannst hier eine vollständige oder teilweise Inventarantwort (Client-Darstellung) einfügen. Alle Felder, die vom Importer unterstützt werden, werden in deinem Account überschrieben.`, import_submit: `Absenden`, prettier_sucks_ass: `` diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 6a465fc2..ba1e406c 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -44,6 +44,14 @@ dict = { code_zanukaA: `Dorma Hound`, code_zanukaB: `Bhaira Hound`, code_zanukaC: `Hec Hound`, + code_stage: `Stage`, + code_complete: `Complete`, + code_nextStage: `Next stage`, + code_prevStage: `Previous stage`, + code_reset: `Reset`, + code_setInactive: `Make the quest inactive`, + code_completed: `Completed`, + code_active: `Active`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, @@ -83,6 +91,11 @@ dict = { inventory_bulkRankUpSentinels: `Max Rank All Sentinels`, inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`, + quests_list: `Quests`, + quests_completeAll: `Complete All Quests`, + quests_resetAll: `Reset All Quests`, + quests_giveAll: `Give All Quests`, + currency_RegularCredits: `Credits`, currency_PremiumCredits: `Platinum`, currency_FusionPoints: `Endo`, @@ -134,12 +147,6 @@ dict = { cheats_changeSupportedSyndicate: `Supported syndicate`, cheats_changeButton: `Change`, cheats_none: `None`, - cheats_quests: `Quests`, - cheats_quests_unlockAll: `Unlock All Quests`, - cheats_quests_completeAll: `Complete All Quests`, - cheats_quests_completeAllUnlocked: `Complete All Unlocked Quests`, - cheats_quests_resetAll: `Reset All Quests`, - cheats_quests_giveAll: `Give All Quests`, import_importNote: `You can provide a full or partial inventory response (client respresentation) here. All fields that are supported by the importer will be overwritten in your account.`, import_submit: `Submit`, prettier_sucks_ass: `` diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 09cf069c..9e307117 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -45,6 +45,14 @@ dict = { code_zanukaA: `Molosse Dorma`, code_zanukaB: `Molosse Bhaira`, code_zanukaC: `Molosse Hec`, + code_stage: `[UNTRANSLATED] Stage`, + code_complete: `[UNTRANSLATED] Complete`, + code_nextStage: `[UNTRANSLATED] Next stage`, + code_prevStage: `[UNTRANSLATED] Previous stage`, + code_reset: `[UNTRANSLATED] Reset`, + code_setInactive: `[UNTRANSLATED] Make the quest inactive`, + code_completed: `[UNTRANSLATED] Completed`, + code_active: `[UNTRANSLATED] Active`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, @@ -84,6 +92,11 @@ dict = { inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`, inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`, + quests_list: `Quêtes`, + quests_completeAll: `Compléter toutes les quêtes`, + quests_resetAll: `Réinitialiser toutes les quêtes`, + quests_giveAll: `Obtenir toutes les quêtes`, + currency_RegularCredits: `Crédits`, currency_PremiumCredits: `Platinum`, currency_FusionPoints: `Endo`, @@ -135,12 +148,6 @@ dict = { cheats_changeSupportedSyndicate: `Allégeance`, cheats_changeButton: `Changer`, cheats_none: `Aucun`, - cheats_quests: `Quêtes`, - cheats_quests_unlockAll: `Débloquer toutes les quêtes`, - cheats_quests_completeAll: `Compléter toutes les quêtes`, - cheats_quests_completeAllUnlocked: `Compléter toutes les quêtes déverrouillées`, - cheats_quests_resetAll: `Réinitialiser toutes les quêtes`, - cheats_quests_giveAll: `Obtenir toutes les quêtes`, import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire écraseront celles présentes dans la base de données.`, import_submit: `Soumettre`, prettier_sucks_ass: `` diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index a10f72e4..294b526e 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -45,6 +45,14 @@ dict = { code_zanukaA: `Гончая: Дорма`, code_zanukaB: `Гончая: Бхайра`, code_zanukaC: `Гончая: Хек`, + code_stage: `Этап`, + code_complete: `Завершить`, + code_nextStage: `Cледующий этап`, + code_prevStage: `Предыдущий этап`, + code_reset: `Сбросить`, + code_setInactive: `Сделать квест неактивным`, + code_completed: `Завершено`, + code_active: `Активный`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, @@ -84,6 +92,11 @@ dict = { inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`, inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`, + quests_list: `Квесты`, + quests_completeAll: `Завершить все квесты`, + quests_resetAll: `Сбросить прогресс всех квестов`, + quests_giveAll: `Выдать все квесты`, + currency_RegularCredits: `Кредиты`, currency_PremiumCredits: `Платина`, currency_FusionPoints: `Эндо`, @@ -135,12 +148,6 @@ dict = { cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`, cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, - cheats_quests: `Квесты`, - cheats_quests_unlockAll: `Разблокировать все квесты`, - cheats_quests_completeAll: `Завершить все квесты`, - cheats_quests_completeAllUnlocked: `Завершить все разблокированые квесты`, - cheats_quests_resetAll: `Сбросить прогресс всех квестов`, - cheats_quests_giveAll: `Выдать все квесты`, import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля будут перезаписаны в вашем аккаунте.`, import_submit: `Отправить`, prettier_sucks_ass: `` diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index a99c30d1..5ec64622 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -45,6 +45,14 @@ dict = { code_zanukaA: `铎玛猎犬`, code_zanukaB: `拜拉猎犬`, code_zanukaC: `骸克猎犬`, + code_stage: `[UNTRANSLATED] Stage`, + code_complete: `[UNTRANSLATED] Complete`, + code_nextStage: `[UNTRANSLATED] Next stage`, + code_prevStage: `[UNTRANSLATED] Previous stage`, + code_reset: `[UNTRANSLATED] Reset`, + code_setInactive: `[UNTRANSLATED] Make the quest inactive`, + code_completed: `[UNTRANSLATED] Completed`, + code_active: `[UNTRANSLATED] Active`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, @@ -84,6 +92,11 @@ dict = { inventory_bulkRankUpSentinels: `所有守护升满级`, inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`, + quests_list: `任务`, + quests_completeAll: `完成所有任务`, + quests_resetAll: `重置所有任务`, + quests_giveAll: `授予所有任务`, + currency_RegularCredits: `现金`, currency_PremiumCredits: `白金`, currency_FusionPoints: `内融核心`, @@ -135,12 +148,6 @@ dict = { cheats_changeSupportedSyndicate: `支持的集团`, cheats_changeButton: `更改`, cheats_none: `无`, - cheats_quests: `任务`, - cheats_quests_unlockAll: `解锁所有任务`, - cheats_quests_completeAll: `完成所有任务`, - cheats_quests_completeAllUnlocked: `完成所有已解锁任务`, - cheats_quests_resetAll: `重置所有任务`, - cheats_quests_giveAll: `授予所有任务`, import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段将被覆盖到您的账户中。`, import_submit: `提交`, prettier_sucks_ass: `` From abeb17ce44c1fdc89d7744b6fe1b2d681cba78ec Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:41:01 -0700 Subject: [PATCH 362/776] chore: add alliance information to getAccountInfo (#1439) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1439 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../custom/getAccountInfoController.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/controllers/custom/getAccountInfoController.ts b/src/controllers/custom/getAccountInfoController.ts index 5d83b56b..0f6524ad 100644 --- a/src/controllers/custom/getAccountInfoController.ts +++ b/src/controllers/custom/getAccountInfoController.ts @@ -1,4 +1,4 @@ -import { Guild, GuildMember } from "@/src/models/guildModel"; +import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; import { RequestHandler } from "express"; @@ -12,9 +12,19 @@ export const getAccountInfoController: RequestHandler = async (req, res) => { } const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); if (guildMember) { - const guild = (await Guild.findById(guildMember.guildId, "Ranks"))!; + const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!; info.GuildId = guildMember.guildId.toString(); info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions; + info.GuildRank = guildMember.rank; + if (guild.AllianceId) { + //const alliance = (await Alliance.findById(guild.AllianceId))!; + const allianceMember = (await AllianceMember.findOne({ + allianceId: guild.AllianceId, + guildId: guild._id + }))!; + info.AllianceId = guild.AllianceId.toString(); + info.AlliancePermissions = allianceMember.Permissions; + } } res.json(info); }; @@ -24,4 +34,7 @@ interface IAccountInfo { IsAdministrator?: boolean; GuildId?: string; GuildPermissions?: number; + GuildRank?: number; + AllianceId?: string; + AlliancePermissions?: number; } From 0b18932dd8000dda681c347885c95ad52bf27c90 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 02:46:32 +0200 Subject: [PATCH 363/776] chore: remove duplicate conditional --- src/services/missionInventoryUpdateService.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 0ff1e1d4..2d9face7 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -77,21 +77,23 @@ export const addMissionInventoryUpdates = async ( inventoryUpdates: IMissionInventoryUpdateRequest ): Promise => { const inventoryChanges: IInventoryChanges = {}; - if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.periodicMissionTag) { - const tag = inventoryUpdates.RewardInfo.periodicMissionTag; - const existingCompletion = inventory.PeriodicMissionCompletions.find(completion => completion.tag === tag); + if (inventoryUpdates.RewardInfo) { + if (inventoryUpdates.RewardInfo.periodicMissionTag) { + const tag = inventoryUpdates.RewardInfo.periodicMissionTag; + const existingCompletion = inventory.PeriodicMissionCompletions.find(completion => completion.tag === tag); - if (existingCompletion) { - existingCompletion.date = new Date(); - } else { - inventory.PeriodicMissionCompletions.push({ - tag: tag, - date: new Date() - }); + if (existingCompletion) { + existingCompletion.date = new Date(); + } else { + inventory.PeriodicMissionCompletions.push({ + tag: tag, + date: new Date() + }); + } + } + if (inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { + inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } - } - if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { - inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } if ( inventoryUpdates.MissionFailed && From 92e8ffd7099fc0328dd58c47d0a20d26fd3f1c8a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:04:21 -0700 Subject: [PATCH 364/776] feat: alliance invites (#1452) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1452 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/addToAllianceController.ts | 117 ++++++++++++++++++ src/controllers/api/addToGuildController.ts | 2 +- .../confirmAllianceInvitationController.ts | 37 ++++++ .../api/declineAllianceInviteController.ts | 17 +++ src/helpers/stringHelpers.ts | 18 +++ src/models/guildModel.ts | 8 +- src/routes/api.ts | 6 + src/types/guildTypes.ts | 2 - 8 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/controllers/api/addToAllianceController.ts create mode 100644 src/controllers/api/confirmAllianceInvitationController.ts create mode 100644 src/controllers/api/declineAllianceInviteController.ts diff --git a/src/controllers/api/addToAllianceController.ts b/src/controllers/api/addToAllianceController.ts new file mode 100644 index 00000000..e7b24dec --- /dev/null +++ b/src/controllers/api/addToAllianceController.ts @@ -0,0 +1,117 @@ +import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers"; +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const addToAllianceController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + // Check guild has invite permissions in the alliance + const allianceMember = (await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }))!; + if (!(allianceMember.Permissions & GuildPermission.Recruiter)) { + res.status(400).json({ Error: 104 }); + return; + } + + // Find clan to invite + const payload = getJSONfromString(String(req.body)); + const guilds = await Guild.find( + { + Name: + payload.clanName.indexOf("#") == -1 + ? new RegExp("^" + regexEscape(payload.clanName) + "#...$") + : payload.clanName + }, + "Name" + ); + if (guilds.length == 0) { + res.status(400).json({ Error: 101 }); + return; + } + if (guilds.length > 1) { + const choices: IGuildChoice[] = []; + for (const guild of guilds) { + choices.push({ + OriginalPlatform: 0, + Name: guild.Name + }); + } + res.json(choices); + return; + } + + // Add clan as a pending alliance member + try { + await AllianceMember.insertOne({ + allianceId: req.query.allianceId, + guildId: guilds[0]._id, + Pending: true, + Permissions: 0 + }); + } catch (e) { + logger.debug(`alliance invite failed due to ${String(e)}`); + res.status(400).json({ Error: 102 }); + return; + } + + // Send inbox message to founding warlord + // TOVERIFY: Should other warlords get this as well? + // TOVERIFY: Who/what should the sender be? + // TOVERIFY: Should this message be highPriority? + const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!; + const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType"); + const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!; + const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!; + await createMessage(invitedClanOwnerMember.accountId, [ + { + sndr: getSuffixedName(account), + msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body", + arg: [ + { + Key: "THEIR_CLAN", + Tag: senderGuild.Name + }, + { + Key: "CLAN", + Tag: guilds[0].Name + }, + { + Key: "ALLIANCE", + Tag: alliance.Name + } + ], + sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + contextInfo: alliance._id.toString(), + highPriority: true, + acceptAction: "ALLIANCE_INVITE", + declineAction: "ALLIANCE_INVITE", + hasAccountAction: true + } + ]); + + res.end(); +}; + +interface IAddToAllianceRequest { + clanName: string; +} + +interface IGuildChoice { + OriginalPlatform: number; + Name: string; +} diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index d2a89df4..c67b8d1a 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -88,7 +88,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable. }); } catch (e) { - logger.debug(`alliance invite failed due to ${String(e)}`); + logger.debug(`guild invite failed due to ${String(e)}`); res.status(400).send("Already requested"); } res.end(); diff --git a/src/controllers/api/confirmAllianceInvitationController.ts b/src/controllers/api/confirmAllianceInvitationController.ts new file mode 100644 index 00000000..8d998e77 --- /dev/null +++ b/src/controllers/api/confirmAllianceInvitationController.ts @@ -0,0 +1,37 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const confirmAllianceInvitationController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const accountId = await getAccountIdForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + const allianceMember = await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }); + if (!allianceMember || !allianceMember.Pending) { + res.status(400); + return; + } + allianceMember.Pending = false; + + const guild = (await Guild.findById(guildMember.guildId))!; + guild.AllianceId = allianceMember.allianceId; + + await Promise.all([allianceMember.save(), guild.save()]); + + // Give client the new alliance data which uses "AllianceId" instead of "_id" in this response + const alliance = (await Alliance.findById(allianceMember.allianceId))!; + const { _id, ...rest } = await getAllianceClient(alliance, guild); + res.json({ + AllianceId: _id, + ...rest + }); +}; diff --git a/src/controllers/api/declineAllianceInviteController.ts b/src/controllers/api/declineAllianceInviteController.ts new file mode 100644 index 00000000..2d9f9dd6 --- /dev/null +++ b/src/controllers/api/declineAllianceInviteController.ts @@ -0,0 +1,17 @@ +import { AllianceMember, GuildMember } from "@/src/models/guildModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const declineAllianceInviteController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const accountId = await getAccountIdForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId }); + + res.end(); +}; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index aee4355c..f24fab32 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -26,3 +26,21 @@ export const getIndexAfter = (str: string, searchWord: string): number => { } return index + searchWord.length; }; + +export const regexEscape = (str: string): string => { + str = str.split(".").join("\\."); + str = str.split("\\").join("\\\\"); + str = str.split("[").join("\\["); + str = str.split("]").join("\\]"); + str = str.split("+").join("\\+"); + str = str.split("*").join("\\*"); + str = str.split("$").join("\\$"); + str = str.split("^").join("\\^"); + str = str.split("?").join("\\?"); + str = str.split("|").join("\\|"); + str = str.split("(").join("\\("); + str = str.split(")").join("\\)"); + str = str.split("{").join("\\{"); + str = str.split("}").join("\\}"); + return str; +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index cce21847..763e6241 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -265,10 +265,10 @@ allianceSchema.index({ Name: 1 }, { unique: true }); export const Alliance = model("Alliance", allianceSchema); const allianceMemberSchema = new Schema({ - allianceId: Schema.Types.ObjectId, - guildId: Schema.Types.ObjectId, - Pending: Boolean, - Permissions: Number + allianceId: { type: Schema.Types.ObjectId, required: true }, + guildId: { type: Schema.Types.ObjectId, required: true }, + Pending: { type: Boolean, required: true }, + Permissions: { type: Number, required: true } }); allianceMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); diff --git a/src/routes/api.ts b/src/routes/api.ts index cadce506..82d93143 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; @@ -18,6 +19,7 @@ import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/cla import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController"; import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController"; import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; @@ -27,6 +29,7 @@ import { createGuildController } from "@/src/controllers/api/createGuildControll import { creditsController } from "@/src/controllers/api/creditsController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; +import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController"; import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; @@ -140,8 +143,10 @@ apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementControlle apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); +apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController); apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.get("/credits.php", creditsController); +apiRouter.get("/declineAllianceInvite.php", declineAllianceInviteController); apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); apiRouter.get("/deleteSession.php", deleteSessionController); apiRouter.get("/dojo", dojoController); @@ -181,6 +186,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 21cb16c6..d40c605c 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -315,8 +315,6 @@ export interface IAllianceMemberDatabase { Permissions: number; } -// TODO: Alliance chat permissions -// TODO: POST /api/addToAlliance.php: {"clanName":"abc"} // TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=1 // TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 // TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 From 61062e433f41be18460e254be7d79c70698aec3d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 06:02:40 -0700 Subject: [PATCH 365/776] feat: personal decos in dojo & move dojo decos (#1451) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1451 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 +- package.json | 2 +- .../api/contributeToVaultController.ts | 14 ++-- .../api/placeDecoInComponentController.ts | 53 +++++++++---- src/models/guildModel.ts | 5 +- src/models/personalRoomsModel.ts | 2 +- src/services/guildService.ts | 78 ++++++++++++++++--- src/services/shipCustomizationsService.ts | 14 ++++ src/types/guildTypes.ts | 3 + src/types/shipTypes.ts | 4 +- 10 files changed, 140 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0be5183e..890754cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.49", + "warframe-public-export-plus": "^0.5.50", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.49", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.49.tgz", - "integrity": "sha512-11HA8qEMhFfl12W2qIjjk7fhas+/5G2yXbrOEb8FRZby6tWka0CyUnB6tLT+PCqBEIoU+kwhz0g7CLh3Zmy7Pw==" + "version": "0.5.50", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.50.tgz", + "integrity": "sha512-KlhdY/Q5sRAIn/RhmdviKBoX3gk+Jtuen0cWnFB2zqK7eKYMDtd79bKOtTPtnK9zCNzh6gFug2wEeDVam3Bwlw==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index e0eb4c8f..36bf7738 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.49", + "warframe-public-export-plus": "^0.5.50", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index fc03e2ca..507d1aa7 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,7 +1,10 @@ import { Alliance, GuildMember } from "@/src/models/guildModel"; import { addGuildMemberMiscItemContribution, + addGuildMemberShipDecoContribution, + addVaultFusionTreasures, addVaultMiscItems, + addVaultShipDecos, getGuildForRequestEx } from "@/src/services/guildService"; import { @@ -51,26 +54,21 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { } if (request.MiscItems.length) { addVaultMiscItems(guild, request.MiscItems); - for (const item of request.MiscItems) { addGuildMemberMiscItemContribution(guildMember, item); - addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } if (request.ShipDecorations.length) { - guild.VaultShipDecorations ??= []; - guildMember.ShipDecorationsContributed ??= []; + addVaultShipDecos(guild, request.ShipDecorations); for (const item of request.ShipDecorations) { - guild.VaultShipDecorations.push(item); - guildMember.ShipDecorationsContributed.push(item); + addGuildMemberShipDecoContribution(guildMember, item); addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } if (request.FusionTreasures.length) { - guild.VaultFusionTreasures ??= []; + addVaultFusionTreasures(guild, request.FusionTreasures); for (const item of request.FusionTreasures) { - guild.VaultFusionTreasures.push(item); addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index 3a09ddfc..deaefc06 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -12,7 +12,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; import { Types } from "mongoose"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus"; export const placeDecoInComponentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -32,23 +32,37 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = } component.Decos ??= []; - const deco = - component.Decos[ - component.Decos.push({ - _id: new Types.ObjectId(), - Type: request.Type, - Pos: request.Pos, - Rot: request.Rot, - Name: request.Name - }) - 1 - ]; - - const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type); - if (meta) { - if (meta.capacityCost) { - component.DecoCapacity -= meta.capacityCost; + if (request.MoveId) { + const deco = component.Decos.find(x => x._id.equals(request.MoveId))!; + deco.Pos = request.Pos; + deco.Rot = request.Rot; + } else { + const deco = + component.Decos[ + component.Decos.push({ + _id: new Types.ObjectId(), + Type: request.Type, + Pos: request.Pos, + Rot: request.Rot, + Name: request.Name, + Sockets: request.Sockets + }) - 1 + ]; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type); + if (meta) { + if (meta.capacityCost) { + component.DecoCapacity -= meta.capacityCost; + } + } else { + const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0]; + if (deco.Sockets !== undefined) { + guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -= + 1; + } else { + guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1; + } } - if (meta.price == 0 && meta.ingredients.length == 0) { + if (!meta || (meta.price == 0 && meta.ingredients.length == 0)) { deco.CompletionTime = new Date(); } else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { @@ -94,4 +108,9 @@ interface IPlaceDecoInComponentRequest { Pos: number[]; Rot: number[]; Name?: string; + Sockets?: number; + Scale?: number; // only provided alongside MoveId and seems to always be 1 + MoveId?: string; + ShipDeco?: boolean; + VaultDeco?: boolean; } diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 763e6241..fad1e0e8 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -17,16 +17,19 @@ import { } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; +import { pictureFrameInfoSchema } from "./personalRoomsModel"; const dojoDecoSchema = new Schema({ Type: String, Pos: [Number], Rot: [Number], Name: String, + Sockets: Number, RegularCredits: Number, MiscItems: { type: [typeCountSchema], default: undefined }, CompletionTime: Date, - RushPlatinum: Number + RushPlatinum: Number, + PictureFrameInfo: pictureFrameInfoSchema }); const dojoLeaderboardEntrySchema = new Schema( diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index b8049d6f..1c6a7c6d 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -12,7 +12,7 @@ import { } from "@/src/types/shipTypes"; import { Schema, model } from "mongoose"; -const pictureFrameInfoSchema = new Schema( +export const pictureFrameInfoSchema = new Schema( { Image: String, Filter: String, diff --git a/src/services/guildService.ts b/src/services/guildService.ts index bb646b46..0e05bb94 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -21,13 +21,13 @@ import { } from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { Types } from "mongoose"; -import { ExportDojoRecipes, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; +import { ExportDojoRecipes, ExportResources, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; import { config } from "./configService"; import { Account } from "../models/loginModel"; import { getRandomInt } from "./rngService"; import { Inbox } from "../models/inboxModel"; -import { ITypeCount } from "../types/inventoryTypes/inventoryTypes"; +import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "../types/purchaseTypes"; export const getGuildForRequest = async (req: Request): Promise => { @@ -202,7 +202,9 @@ export const getDojoClient = async ( Type: deco.Type, Pos: deco.Pos, Rot: deco.Rot, - Name: deco.Name + Name: deco.Name, + Sockets: deco.Sockets, + PictureFrameInfo: deco.PictureFrameInfo }; if (deco.CompletionTime) { clientDeco.CompletionTime = toMongoDate(deco.CompletionTime); @@ -285,8 +287,28 @@ export const removeDojoDeco = ( 1 )[0]; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type); - if (meta && meta.capacityCost) { - component.DecoCapacity! += meta.capacityCost; + if (meta) { + if (meta.capacityCost) { + component.DecoCapacity! += meta.capacityCost; + } + } else { + const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0]; + if (deco.Sockets !== undefined) { + addVaultFusionTreasures(guild, [ + { + ItemType: itemType, + ItemCount: 1, + Sockets: deco.Sockets + } + ]); + } else { + addVaultShipDecos(guild, [ + { + ItemType: itemType, + ItemCount: 1 + } + ]); + } } moveResourcesToVault(guild, deco); }; @@ -311,12 +333,38 @@ export const getVaultMiscItemCount = (guild: TGuildDatabaseDocument, itemType: s export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => { guild.VaultMiscItems ??= []; - for (const miscItem of miscItems) { - const vaultMiscItem = guild.VaultMiscItems.find(x => x.ItemType == miscItem.ItemType); - if (vaultMiscItem) { - vaultMiscItem.ItemCount += miscItem.ItemCount; + for (const item of miscItems) { + const vaultItem = guild.VaultMiscItems.find(x => x.ItemType == item.ItemType); + if (vaultItem) { + vaultItem.ItemCount += item.ItemCount; } else { - guild.VaultMiscItems.push(miscItem); + guild.VaultMiscItems.push(item); + } + } +}; + +export const addVaultShipDecos = (guild: TGuildDatabaseDocument, shipDecos: ITypeCount[]): void => { + guild.VaultShipDecorations ??= []; + for (const item of shipDecos) { + const vaultItem = guild.VaultShipDecorations.find(x => x.ItemType == item.ItemType); + if (vaultItem) { + vaultItem.ItemCount += item.ItemCount; + } else { + guild.VaultShipDecorations.push(item); + } + } +}; + +export const addVaultFusionTreasures = (guild: TGuildDatabaseDocument, fusionTreasures: IFusionTreasure[]): void => { + guild.VaultFusionTreasures ??= []; + for (const item of fusionTreasures) { + const vaultItem = guild.VaultFusionTreasures.find( + x => x.ItemType == item.ItemType && x.Sockets == item.Sockets + ); + if (vaultItem) { + vaultItem.ItemCount += item.ItemCount; + } else { + guild.VaultFusionTreasures.push(item); } } }; @@ -331,6 +379,16 @@ export const addGuildMemberMiscItemContribution = (guildMember: IGuildMemberData } }; +export const addGuildMemberShipDecoContribution = (guildMember: IGuildMemberDatabase, item: ITypeCount): void => { + guildMember.ShipDecorationsContributed ??= []; + const shipDecoContribution = guildMember.ShipDecorationsContributed.find(x => x.ItemType == item.ItemType); + if (shipDecoContribution) { + shipDecoContribution.ItemCount += item.ItemCount; + } else { + guildMember.ShipDecorationsContributed.push(item); + } +}; + export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { if (build.guildXpValue) { guild.ClaimedXP ??= []; diff --git a/src/services/shipCustomizationsService.ts b/src/services/shipCustomizationsService.ts index 20c3d4ca..47764917 100644 --- a/src/services/shipCustomizationsService.ts +++ b/src/services/shipCustomizationsService.ts @@ -10,6 +10,9 @@ import { logger } from "@/src/utils/logger"; import { Types } from "mongoose"; import { addShipDecorations, getInventory } from "./inventoryService"; import { config } from "./configService"; +import { Guild } from "../models/guildModel"; +import { hasGuildPermission } from "./guildService"; +import { GuildPermission } from "../types/guildTypes"; export const setShipCustomizations = async ( accountId: string, @@ -154,6 +157,17 @@ export const handleSetShipDecorations = async ( }; export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise => { + if (req.GuildId && req.ComponentId) { + const guild = (await Guild.findById(req.GuildId))!; + if (await hasGuildPermission(guild, accountId, GuildPermission.Decorator)) { + const component = guild.DojoComponents.id(req.ComponentId)!; + const deco = component.Decos!.find(x => x._id.equals(req.DecoId))!; + deco.PictureFrameInfo = req.PictureFrameInfo; + await guild.save(); + } + return; + } + const personalRooms = await getPersonalRooms(accountId); const room = personalRooms.Ship.Rooms.find(room => room.Name === req.Room); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index d40c605c..949f87ce 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -1,6 +1,7 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "@/src/types/commonTypes"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IPictureFrameInfo } from "./shipTypes"; export interface IGuildClient { _id: IOid; @@ -191,10 +192,12 @@ export interface IDojoDecoClient { Pos: number[]; Rot: number[]; Name?: string; // for teleporters + Sockets?: number; RegularCredits?: number; MiscItems?: IMiscItem[]; CompletionTime?: IMongoDate; RushPlatinum?: number; + PictureFrameInfo?: IPictureFrameInfo; } export interface IDojoDecoDatabase extends Omit { diff --git a/src/types/shipTypes.ts b/src/types/shipTypes.ts index 936a5cc6..23c46c48 100644 --- a/src/types/shipTypes.ts +++ b/src/types/shipTypes.ts @@ -127,7 +127,9 @@ export interface ISetPlacedDecoInfoRequest { DecoId: string; Room: string; PictureFrameInfo: IPictureFrameInfo; - BootLocation: string; + BootLocation?: string; + ComponentId?: string; + GuildId?: string; } export interface IPictureFrameInfo { From c18abab9c4035d40069eb2c36db086a0a8e2b81e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 06:02:55 -0700 Subject: [PATCH 366/776] feat: handle miscItemFee in end of match upload (#1454) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1454 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2d9face7..70ca6eea 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -77,6 +77,21 @@ export const addMissionInventoryUpdates = async ( inventoryUpdates: IMissionInventoryUpdateRequest ): Promise => { const inventoryChanges: IInventoryChanges = {}; + if ( + inventoryUpdates.EndOfMatchUpload && + inventoryUpdates.Missions && + inventoryUpdates.Missions.Tag in ExportRegions + ) { + const node = ExportRegions[inventoryUpdates.Missions.Tag]; + if (node.miscItemFee) { + addMiscItems(inventory, [ + { + ItemType: node.miscItemFee.ItemType, + ItemCount: node.miscItemFee.ItemCount * -1 + } + ]); + } + } if (inventoryUpdates.RewardInfo) { if (inventoryUpdates.RewardInfo.periodicMissionTag) { const tag = inventoryUpdates.RewardInfo.periodicMissionTag; From b3374eb66e2d8619d26d15f6b6e075a599de8537 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 06:03:12 -0700 Subject: [PATCH 367/776] feat: divvy alliance vault (#1455) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1455 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/divvyAllianceVaultController.ts | 74 +++++++++++++++++++ src/routes/api.ts | 2 + src/types/guildTypes.ts | 2 - 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/divvyAllianceVaultController.ts diff --git a/src/controllers/api/divvyAllianceVaultController.ts b/src/controllers/api/divvyAllianceVaultController.ts new file mode 100644 index 00000000..e6f786e9 --- /dev/null +++ b/src/controllers/api/divvyAllianceVaultController.ts @@ -0,0 +1,74 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const divvyAllianceVaultController: RequestHandler = async (req, res) => { + // Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits. + if (req.query.credits == "1") { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).end(); + return; + } + + // Check guild has treasurer permissions in the alliance + const allianceMember = (await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }))!; + if (!(allianceMember.Permissions & GuildPermission.Treasurer)) { + res.status(400).end(); + return; + } + + const allianceMembers = await AllianceMember.find({ allianceId: req.query.allianceId }); + const memberCounts: Record = {}; + let totalMembers = 0; + await parallelForeach(allianceMembers, async allianceMember => { + const memberCount = await GuildMember.countDocuments({ + guildId: allianceMember.guildId + }); + memberCounts[allianceMember.guildId.toString()] = memberCount; + totalMembers += memberCount; + }); + logger.debug(`alliance has ${totalMembers} members between all its clans`); + + const alliance = (await Alliance.findById(allianceMember.allianceId, "VaultRegularCredits"))!; + if (alliance.VaultRegularCredits) { + let creditsHandedOutInTotal = 0; + await parallelForeach(allianceMembers, async allianceMember => { + const memberCount = memberCounts[allianceMember.guildId.toString()]; + const cutPercentage = memberCount / totalMembers; + const creditsToHandOut = Math.trunc(alliance.VaultRegularCredits! * cutPercentage); + logger.debug( + `${allianceMember.guildId.toString()} has ${memberCount} member(s) = ${Math.trunc(cutPercentage * 100)}% of alliance; giving ${creditsToHandOut} credit(s)` + ); + if (creditsToHandOut != 0) { + await Guild.updateOne( + { _id: allianceMember.guildId }, + { $inc: { VaultRegularCredits: creditsToHandOut } } + ); + creditsHandedOutInTotal += creditsToHandOut; + } + }); + alliance.VaultRegularCredits -= creditsHandedOutInTotal; + logger.debug( + `handed out ${creditsHandedOutInTotal} credits; alliance vault now has ${alliance.VaultRegularCredits} credit(s)` + ); + } + await alliance.save(); + } + res.end(); +}; + +const parallelForeach = async (data: T[], op: (datum: T) => Promise): Promise => { + const promises: Promise[] = []; + for (const datum of data) { + promises.push(op(datum)); + } + await Promise.all(promises); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 82d93143..d25efef0 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -33,6 +33,7 @@ import { declineAllianceInviteController } from "@/src/controllers/api/declineAl import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; +import { divvyAllianceVaultController } from "@/src/controllers/api/divvyAllianceVaultController"; import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; import { dojoController } from "@/src/controllers/api/dojoController"; import { dronesController } from "@/src/controllers/api/dronesController"; @@ -149,6 +150,7 @@ apiRouter.get("/credits.php", creditsController); apiRouter.get("/declineAllianceInvite.php", declineAllianceInviteController); apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); apiRouter.get("/deleteSession.php", deleteSessionController); +apiRouter.get("/divvyAllianceVault.php", divvyAllianceVaultController); apiRouter.get("/dojo", dojoController); apiRouter.get("/drones.php", dronesController); apiRouter.get("/getDailyDealStockLevels.php", getDailyDealStockLevelsController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 949f87ce..d778aa07 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -318,7 +318,5 @@ export interface IAllianceMemberDatabase { Permissions: number; } -// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=1 -// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 // TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 // TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 From 2746e243c9a2c7f867e2b42f06c3aab7c5bd6816 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:12:25 +0200 Subject: [PATCH 368/776] chore: add async-utils --- src/controllers/api/divvyAllianceVaultController.ts | 9 +-------- src/utils/async-utils.ts | 7 +++++++ 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/utils/async-utils.ts diff --git a/src/controllers/api/divvyAllianceVaultController.ts b/src/controllers/api/divvyAllianceVaultController.ts index e6f786e9..8847a19f 100644 --- a/src/controllers/api/divvyAllianceVaultController.ts +++ b/src/controllers/api/divvyAllianceVaultController.ts @@ -1,6 +1,7 @@ import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { getAccountForRequest } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; @@ -64,11 +65,3 @@ export const divvyAllianceVaultController: RequestHandler = async (req, res) => } res.end(); }; - -const parallelForeach = async (data: T[], op: (datum: T) => Promise): Promise => { - const promises: Promise[] = []; - for (const datum of data) { - promises.push(op(datum)); - } - await Promise.all(promises); -}; diff --git a/src/utils/async-utils.ts b/src/utils/async-utils.ts new file mode 100644 index 00000000..b2d40c0d --- /dev/null +++ b/src/utils/async-utils.ts @@ -0,0 +1,7 @@ +export const parallelForeach = async (data: T[], op: (datum: T) => Promise): Promise => { + const promises: Promise[] = []; + for (const datum of data) { + promises.push(op(datum)); + } + await Promise.all(promises); +}; From d5ff34974617e9e9bbed4169daaaf55883f74be4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:16:46 -0700 Subject: [PATCH 369/776] fix: update TradesRemaining at daily reset (#1457) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1457 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index cef69be7..ab0391a6 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -34,6 +34,7 @@ export const inventoryController: RequestHandler = async (request, response) => } inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); + inventory.TradesRemaining = inventory.PlayerLevel; inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); From d94b4fd946203e97554e310c4f14dbac8346417c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:16:57 -0700 Subject: [PATCH 370/776] chore: use parallelForeach in deleteGuild (#1458) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1458 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/guildService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 0e05bb94..532496aa 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -29,6 +29,7 @@ import { getRandomInt } from "./rngService"; import { Inbox } from "../models/inboxModel"; import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "../types/purchaseTypes"; +import { parallelForeach } from "../utils/async-utils"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -595,12 +596,12 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise => { await Guild.deleteOne({ _id: guildId }); const guildMembers = await GuildMember.find({ guildId, status: 0 }, "accountId"); - for (const member of guildMembers) { + await parallelForeach(guildMembers, async member => { const inventory = await getInventory(member.accountId.toString(), "GuildId LevelKeys Recipes"); inventory.GuildId = undefined; removeDojoKeyItems(inventory); await inventory.save(); - } + }); await GuildMember.deleteMany({ guildId }); From 23267aa64179dec054a160baa35010f7e19f905b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:18:10 -0700 Subject: [PATCH 371/776] feat: leave alliance/kick alliance members (#1459) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1459 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/removeFromAllianceController.ts | 38 +++++++++++++++++++ src/routes/api.ts | 2 + src/services/guildService.ts | 23 +++++++++-- src/types/guildTypes.ts | 5 +-- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/controllers/api/removeFromAllianceController.ts diff --git a/src/controllers/api/removeFromAllianceController.ts b/src/controllers/api/removeFromAllianceController.ts new file mode 100644 index 00000000..f6dc8acc --- /dev/null +++ b/src/controllers/api/removeFromAllianceController.ts @@ -0,0 +1,38 @@ +import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { deleteAlliance } from "@/src/services/guildService"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const removeFromAllianceController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + let allianceMember = (await AllianceMember.findOne({ guildId: guildMember.guildId }))!; + if (!guildMember.guildId.equals(req.query.guildId as string)) { + // Removing a guild that is not our own needs additional permissions + if (!(allianceMember.Permissions & GuildPermission.Ruler)) { + res.status(400).json({ Error: 104 }); + return; + } + + // Update allianceMember to point to the alliance to kick + allianceMember = (await AllianceMember.findOne({ guildId: req.query.guildId }))!; + } + + if (allianceMember.Permissions & GuildPermission.Ruler) { + await deleteAlliance(allianceMember.allianceId); + } else { + await Promise.all([ + await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }), + await AllianceMember.deleteOne({ _id: allianceMember._id }) + ]); + } + + res.end(); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index d25efef0..e05602f0 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -92,6 +92,7 @@ import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { releasePetController } from "@/src/controllers/api/releasePetController"; +import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; @@ -173,6 +174,7 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController); apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController); apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); +apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setBootLocation.php", setBootLocationController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 532496aa..2c7d683f 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,7 +1,7 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; -import { AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { GuildPermission, @@ -613,9 +613,26 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise => { await GuildAd.deleteOne({ GuildId: guildId }); - await AllianceMember.deleteMany({ guildId }); + // If guild is the creator of an alliance, delete that as well. + const allianceMember = await AllianceMember.findOne({ guildId, Pending: false }); + if (allianceMember) { + if (allianceMember.Permissions & GuildPermission.Ruler) { + await deleteAlliance(allianceMember.allianceId); + } + } - // TODO: If this guild was the founding guild of an alliance (ruler permission), that would need to be forcefully deleted now as well. + await AllianceMember.deleteMany({ guildId }); +}; + +export const deleteAlliance = async (allianceId: Types.ObjectId): Promise => { + const allianceMembers = await AllianceMember.find({ allianceId, Pending: false }); + await parallelForeach(allianceMembers, async allianceMember => { + await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }); + }); + + await AllianceMember.deleteMany({ allianceId }); + + await Alliance.deleteOne({ _id: allianceId }); }; export const getAllianceClient = async ( diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index d778aa07..f3f69c46 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -71,11 +71,11 @@ export interface ILongMOTD { // 32 seems to be reserved export enum GuildPermission { - Ruler = 1, // Clan: Change hierarchy. Alliance: Kick clans. + Ruler = 1, // Clan: Change hierarchy. Alliance (Creator only): Kick clans. Advertiser = 8192, Recruiter = 2, // Send invites (Clans & Alliances) Regulator = 4, // Kick members - Promoter = 8, // Clan: Promote and demote members. Alliance: Change clan permissions. + Promoter = 8, // Clan: Promote and demote members. Alliance (Creator only): Change clan permissions. Architect = 16, // Create and destroy rooms Decorator = 1024, // Create and destroy decos Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault. @@ -318,5 +318,4 @@ export interface IAllianceMemberDatabase { Permissions: number; } -// TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 // TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 From 1d1abf5550939028b8889caf12784111bddf807a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:05:01 +0200 Subject: [PATCH 372/776] chore: remove unused eslint-disable directives --- src/controllers/api/missionInventoryUpdateController.ts | 1 - src/controllers/api/modularWeaponSaleController.ts | 1 - src/controllers/api/startRecipeController.ts | 1 - src/controllers/api/unlockShipFeatureController.ts | 1 - src/controllers/api/updateQuestController.ts | 1 - 5 files changed, 5 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 0dee93ee..4dc5bd66 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -47,7 +47,6 @@ import { logger } from "@/src/utils/logger"; - [ ] FpsSamples */ //move credit calc in here, return MissionRewards: [] if no reward info -// eslint-disable-next-line @typescript-eslint/no-misused-promises export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise => { const accountId = await getAccountIdForRequest(req); const missionReport = getJSONfromString((req.body as string).toString()); diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index 3e07c1d6..a8ddfc20 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -22,7 +22,6 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => { const partTypeToParts: Record = {}; for (const [uniqueName, data] of Object.entries(ExportWeapons)) { if (data.partType && data.premiumPrice) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition partTypeToParts[data.partType] ??= []; partTypeToParts[data.partType].push(uniqueName); } diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index b313771a..495b8d26 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -90,7 +90,6 @@ export const startRecipeController: RequestHandler = async (req, res) => { spectreLoadout.LongGuns = item.ItemType; spectreLoadout.LongGunsModularParts = item.ModularParts; } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon"); const item = inventory.Melee.id(oid)!; spectreLoadout.Melee = item.ItemType; diff --git a/src/controllers/api/unlockShipFeatureController.ts b/src/controllers/api/unlockShipFeatureController.ts index cdccc2ec..4a3ecd1e 100644 --- a/src/controllers/api/unlockShipFeatureController.ts +++ b/src/controllers/api/unlockShipFeatureController.ts @@ -3,7 +3,6 @@ import { updateShipFeature } from "@/src/services/personalRoomsService"; import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes"; import { parseString } from "@/src/helpers/general"; -// eslint-disable-next-line @typescript-eslint/no-misused-promises export const unlockShipFeatureController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest; diff --git a/src/controllers/api/updateQuestController.ts b/src/controllers/api/updateQuestController.ts index 767528d7..c251094f 100644 --- a/src/controllers/api/updateQuestController.ts +++ b/src/controllers/api/updateQuestController.ts @@ -5,7 +5,6 @@ import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService import { getInventory } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -// eslint-disable-next-line @typescript-eslint/no-misused-promises export const updateQuestController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); const updateQuestRequest = getJSONfromString((req.body as string).toString()); From 651640c4d712b40313e948b203009569c1499b04 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 02:56:29 +0200 Subject: [PATCH 373/776] chore(vscode): recommend eslint extension --- .vscode/extensions.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..897af65d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file From 5c22949c6ba3f3525ed882d6c3552a4f93cba6ac Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:50:57 -0700 Subject: [PATCH 374/776] chore: improve handling when config.json is missing & fix logger options (#1460) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1460 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../custom/renameAccountController.ts | 3 +- .../custom/updateConfigDataController.ts | 2 +- src/index.ts | 16 +++-- src/services/configService.ts | 61 ++++++------------- src/services/configWatcherService.ts | 39 ++++++++++++ src/utils/logger.ts | 10 ++- 6 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 src/services/configWatcherService.ts diff --git a/src/controllers/custom/renameAccountController.ts b/src/controllers/custom/renameAccountController.ts index d30b44ae..5f950550 100644 --- a/src/controllers/custom/renameAccountController.ts +++ b/src/controllers/custom/renameAccountController.ts @@ -1,6 +1,7 @@ import { RequestHandler } from "express"; import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; -import { config, saveConfig } from "@/src/services/configService"; +import { config } from "@/src/services/configService"; +import { saveConfig } from "@/src/services/configWatcherService"; export const renameAccountController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); diff --git a/src/controllers/custom/updateConfigDataController.ts b/src/controllers/custom/updateConfigDataController.ts index 961cccb1..534dfe0f 100644 --- a/src/controllers/custom/updateConfigDataController.ts +++ b/src/controllers/custom/updateConfigDataController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { updateConfig } from "@/src/services/configService"; +import { updateConfig } from "@/src/services/configWatcherService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; const updateConfigDataController: RequestHandler = async (req, res) => { diff --git a/src/index.ts b/src/index.ts index 8c38237f..9a942606 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,30 @@ -import { logger } from "./utils/logger"; +// First, init config. +import { config, loadConfig } from "@/src/services/configService"; +try { + loadConfig(); +} catch (e) { + console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json."); + process.exit(1); +} +// Now we can init the logger with the settings provided in the config. +import { logger } from "@/src/utils/logger"; logger.info("Starting up..."); +// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. import http from "http"; import https from "https"; import fs from "node:fs"; import { app } from "./app"; -import { config, validateConfig } from "./services/configService"; -import { registerLogFileCreationListener } from "@/src/utils/logger"; import mongoose from "mongoose"; import { Json, JSONStringify } from "json-with-bigint"; +import { validateConfig } from "@/src/services/configWatcherService"; // Patch JSON.stringify to work flawlessly with Bigints. JSON.stringify = (obj: Exclude, _replacer?: unknown, space?: string | number): string => { return JSONStringify(obj, space); }; -registerLogFileCreationListener(); validateConfig(); mongoose diff --git a/src/services/configService.ts b/src/services/configService.ts index 6ceaf9d0..c01fee7e 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -1,33 +1,13 @@ import fs from "fs"; -import fsPromises from "fs/promises"; import path from "path"; import { repoDir } from "@/src/helpers/pathHelper"; -import { logger } from "@/src/utils/logger"; - -const configPath = path.join(repoDir, "config.json"); -export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; - -let amnesia = false; -fs.watchFile(configPath, () => { - if (amnesia) { - amnesia = false; - } else { - logger.info("Detected a change to config.json, reloading its contents."); - - // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. - for (const key of Object.keys(config)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (config as any)[key] = undefined; - } - - Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); - validateConfig(); - } -}); interface IConfig { mongodbUrl: string; - logger: ILoggerConfig; + logger: { + files: boolean; + level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; + }; myAddress: string; httpPort?: number; httpsPort?: number; @@ -72,26 +52,23 @@ interface IConfig { }; } -interface ILoggerConfig { - files: boolean; - level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; -} +export const configPath = path.join(repoDir, "config.json"); -export const updateConfig = async (data: string): Promise => { - amnesia = true; - await fsPromises.writeFile(configPath, data); - Object.assign(config, JSON.parse(data)); +export const config: IConfig = { + mongodbUrl: "mongodb://127.0.0.1:27017/openWF", + logger: { + files: true, + level: "trace" + }, + myAddress: "localhost" }; -export const saveConfig = async (): Promise => { - amnesia = true; - await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); -}; - -export const validateConfig = (): void => { - if (typeof config.administratorNames == "string") { - logger.info(`Updating config.json to make administratorNames an array.`); - config.administratorNames = [config.administratorNames]; - void saveConfig(); +export const loadConfig = (): void => { + // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. + for (const key of Object.keys(config)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (config as any)[key] = undefined; } + + Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); }; diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts new file mode 100644 index 00000000..e8584785 --- /dev/null +++ b/src/services/configWatcherService.ts @@ -0,0 +1,39 @@ +import fs from "fs"; +import fsPromises from "fs/promises"; +import { logger } from "../utils/logger"; +import { config, configPath, loadConfig } from "./configService"; + +let amnesia = false; +fs.watchFile(configPath, () => { + if (amnesia) { + amnesia = false; + } else { + logger.info("Detected a change to config.json, reloading its contents."); + try { + loadConfig(); + } catch (e) { + logger.error("Failed to reload config.json. Did you delete it?! Execution cannot continue."); + process.exit(1); + } + validateConfig(); + } +}); + +export const validateConfig = (): void => { + if (typeof config.administratorNames == "string") { + logger.info(`Updating config.json to make administratorNames an array.`); + config.administratorNames = [config.administratorNames]; + void saveConfig(); + } +}; + +export const updateConfig = async (data: string): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, data); + Object.assign(config, JSON.parse(data)); +}; + +export const saveConfig = async (): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index f3873591..f02c0db4 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -104,9 +104,7 @@ export const logger = createLogger({ addColors(logLevels.colors); -export function registerLogFileCreationListener(): void { - errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`)); - combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); - errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); - combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`)); -} +errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`)); +combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); +errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); +combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`)); From 743a905de4fc522e00fdbcbb2992710ebb61aa28 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:51:10 -0700 Subject: [PATCH 375/776] fix: ignore non-weapon entries in ExportWeapons for recipe login reward (#1461) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1461 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/loginRewardService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index d32789b6..7bfc0d71 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -84,7 +84,7 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab } const unmasteredItems = new Set(); for (const [uniqueName, data] of Object.entries(ExportWeapons)) { - if (data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName)) { + if (data.totalDamage != 0 && data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName)) { unmasteredItems.add(uniqueName); } } From 6bb74b026a774bff4f232193b588764b79220c66 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:51:23 -0700 Subject: [PATCH 376/776] feat: contribute to allied clan vault (#1462) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1462 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/contributeToVaultController.ts | 48 ++++++++++++++----- src/models/guildModel.ts | 7 +++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index 507d1aa7..77a93097 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,4 +1,10 @@ -import { Alliance, GuildMember } from "@/src/models/guildModel"; +import { + Alliance, + Guild, + GuildMember, + TGuildDatabaseDocument, + TGuildMemberDatabaseDocument +} from "@/src/models/guildModel"; import { addGuildMemberMiscItemContribution, addGuildMemberShipDecoContribution, @@ -21,10 +27,10 @@ import { RequestHandler } from "express"; export const contributeToVaultController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures"); - const guild = await getGuildForRequestEx(req, inventory); const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; if (request.Alliance) { + const guild = await getGuildForRequestEx(req, inventory); const alliance = (await Alliance.findById(guild.AllianceId!))!; alliance.VaultRegularCredits ??= 0; alliance.VaultRegularCredits += request.RegularCredits; @@ -39,30 +45,44 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { return; } - const guildMember = (await GuildMember.findOne( - { accountId, guildId: guild._id }, - "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed" - ))!; + let guild: TGuildDatabaseDocument; + let guildMember: TGuildMemberDatabaseDocument | undefined; + if (request.GuildVault) { + guild = (await Guild.findById(request.GuildVault))!; + } else { + guild = await getGuildForRequestEx(req, inventory); + guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed" + ))!; + } + if (request.RegularCredits) { updateCurrency(inventory, request.RegularCredits, false); guild.VaultRegularCredits ??= 0; guild.VaultRegularCredits += request.RegularCredits; - guildMember.RegularCreditsContributed ??= 0; - guildMember.RegularCreditsContributed += request.RegularCredits; + if (guildMember) { + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += request.RegularCredits; + } } if (request.MiscItems.length) { addVaultMiscItems(guild, request.MiscItems); for (const item of request.MiscItems) { - addGuildMemberMiscItemContribution(guildMember, item); + if (guildMember) { + addGuildMemberMiscItemContribution(guildMember, item); + } addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } if (request.ShipDecorations.length) { addVaultShipDecos(guild, request.ShipDecorations); for (const item of request.ShipDecorations) { - addGuildMemberShipDecoContribution(guildMember, item); + if (guildMember) { + addGuildMemberShipDecoContribution(guildMember, item); + } addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } } @@ -73,7 +93,12 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { } } - await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + const promises: Promise[] = [guild.save(), inventory.save()]; + if (guildMember) { + promises.push(guildMember.save()); + } + await Promise.all(promises); + res.end(); }; @@ -84,4 +109,5 @@ interface IContributeToVaultRequest { FusionTreasures: IFusionTreasure[]; Alliance?: boolean; FromVault?: boolean; + GuildVault?: string; } diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index fad1e0e8..563cb7d2 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -239,6 +239,13 @@ guildMemberSchema.index({ RequestExpiry: 1 }, { expireAfterSeconds: 0 }); export const GuildMember = model("GuildMember", guildMemberSchema); +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TGuildMemberDatabaseDocument = Document & + IGuildMemberDatabase & { + _id: Types.ObjectId; + __v: number; + }; + const guildAdSchema = new Schema({ GuildId: { type: Schema.Types.ObjectId, required: true }, Emblem: Boolean, From 2ef59cd570a3468eca1cf8ef97a1ed5933d039f6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:51:37 -0700 Subject: [PATCH 377/776] chore: split confirmGuildInvitation get & post controllers (#1465) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1465 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .prettierignore | 1 + .../api/confirmGuildInvitationController.ts | 110 +++++++++--------- src/routes/api.ts | 6 +- 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/.prettierignore b/.prettierignore index 59d6af97..8929f3d1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ +src/routes/api.ts static/webui/libs/ *.html *.md diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts index e65cf9db..9f43b893 100644 --- a/src/controllers/api/confirmGuildInvitationController.ts +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -9,62 +9,8 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; import { Types } from "mongoose"; -export const confirmGuildInvitationController: RequestHandler = async (req, res) => { - if (req.body) { - // POST request: Clan representative accepting invite(s). - const accountId = await getAccountIdForRequest(req); - const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!; - if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) { - res.status(400).json("Invalid permission"); - return; - } - const payload = getJSONfromString<{ userId: string }>(String(req.body)); - const filter: { accountId?: string; status: number } = { status: 1 }; - if (payload.userId != "all") { - filter.accountId = payload.userId; - } - const guildMembers = await GuildMember.find(filter); - const newMembers: string[] = []; - for (const guildMember of guildMembers) { - guildMember.status = 0; - guildMember.RequestMsg = undefined; - guildMember.RequestExpiry = undefined; - await guildMember.save(); - - // Remove other pending applications for this account - await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 }); - - // Update inventory of new member - const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes"); - inventory.GuildId = new Types.ObjectId(req.query.clanId as string); - addRecipes(inventory, [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ]); - await inventory.save(); - - // Add join to clan log - const account = (await Account.findOne({ _id: guildMember.accountId }))!; - guild.RosterActivity ??= []; - guild.RosterActivity.push({ - dateTime: new Date(), - entryType: 6, - details: getSuffixedName(account) - }); - - newMembers.push(account._id.toString()); - } - await guild.save(); - res.json({ - NewMembers: newMembers - }); - return; - } - - // GET request: A player accepting an invite they got in their inbox. - +// GET request: A player accepting an invite they got in their inbox. +export const confirmGuildInvitationGetController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); const invitedGuildMember = await GuildMember.findOne({ accountId: account._id, @@ -124,3 +70,55 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res) res.end(); } }; + +// POST request: Clan representative accepting invite(s). +export const confirmGuildInvitationPostController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!; + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) { + res.status(400).json("Invalid permission"); + return; + } + const payload = getJSONfromString<{ userId: string }>(String(req.body)); + const filter: { accountId?: string; status: number } = { status: 1 }; + if (payload.userId != "all") { + filter.accountId = payload.userId; + } + const guildMembers = await GuildMember.find(filter); + const newMembers: string[] = []; + for (const guildMember of guildMembers) { + guildMember.status = 0; + guildMember.RequestMsg = undefined; + guildMember.RequestExpiry = undefined; + await guildMember.save(); + + // Remove other pending applications for this account + await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 }); + + // Update inventory of new member + const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes"); + inventory.GuildId = new Types.ObjectId(req.query.clanId as string); + addRecipes(inventory, [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]); + await inventory.save(); + + // Add join to clan log + const account = (await Account.findOne({ _id: guildMember.accountId }))!; + guild.RosterActivity ??= []; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 6, + details: getSuffixedName(account) + }); + + newMembers.push(account._id.toString()); + } + await guild.save(); + res.json({ + NewMembers: newMembers + }); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index e05602f0..055ac4f6 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -20,7 +20,7 @@ import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialo import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController"; -import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController"; +import { confirmGuildInvitationGetController, confirmGuildInvitationPostController } from "@/src/controllers/api/confirmGuildInvitationController"; import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; @@ -146,7 +146,7 @@ apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController); -apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController); +apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationGetController); apiRouter.get("/credits.php", creditsController); apiRouter.get("/declineAllianceInvite.php", declineAllianceInviteController); apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); @@ -201,7 +201,7 @@ apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); -apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationController); +apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationPostController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/contributeToVault.php", contributeToVaultController); From f66c958a3c3bb29c938938a25510466d3f25d7d6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:51:54 -0700 Subject: [PATCH 378/776] feat: change alliance member permissions (#1466) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1466 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../setAllianceGuildPermissionsController.ts | 38 +++++++++++++++++++ src/routes/api.ts | 2 + src/types/guildTypes.ts | 2 - 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/setAllianceGuildPermissionsController.ts diff --git a/src/controllers/api/setAllianceGuildPermissionsController.ts b/src/controllers/api/setAllianceGuildPermissionsController.ts new file mode 100644 index 00000000..ce3caaf8 --- /dev/null +++ b/src/controllers/api/setAllianceGuildPermissionsController.ts @@ -0,0 +1,38 @@ +import { AllianceMember, GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const setAllianceGuildPermissionsController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).end(); + return; + } + + // Check guild is the creator of the alliance and don't allow changing of own permissions. (Technically changing permissions requires the Promoter permission, but both are exclusive to the creator guild.) + const allianceMember = (await AllianceMember.findOne({ + guildId: guildMember.guildId, + Pending: false + }))!; + if ( + !(allianceMember.Permissions & GuildPermission.Ruler) || + allianceMember.guildId.equals(req.query.guildId as string) + ) { + res.status(400).end(); + return; + } + + const targetAllianceMember = (await AllianceMember.findOne({ + allianceId: allianceMember.allianceId, + guildId: req.query.guildId + }))!; + targetAllianceMember.Permissions = + parseInt(req.query.perms as string) & + (GuildPermission.Recruiter | GuildPermission.Treasurer | GuildPermission.ChatModerator); + await targetAllianceMember.save(); + + res.end(); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 055ac4f6..81415dc6 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -103,6 +103,7 @@ import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVau import { sellController } from "@/src/controllers/api/sellController"; import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController"; import { setActiveShipController } from "@/src/controllers/api/setActiveShipController"; +import { setAllianceGuildPermissionsController } from "@/src/controllers/api/setAllianceGuildPermissionsController"; import { setBootLocationController } from "@/src/controllers/api/setBootLocationController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; @@ -177,6 +178,7 @@ apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructio apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); +apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController); apiRouter.get("/setBootLocation.php", setBootLocationController); apiRouter.get("/setGuildMotd.php", setGuildMotdController); apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index f3f69c46..d7d6d66d 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -317,5 +317,3 @@ export interface IAllianceMemberDatabase { Pending: boolean; Permissions: number; } - -// TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 From 62263efde3d73e2693f7739636ee5f02d8e760c4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:52:13 -0700 Subject: [PATCH 379/776] chore: simplify serversideVendorsService's api (#1467) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1467 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/getVendorInfoController.ts | 4 ++-- .../api/postGuildAdvertisementController.ts | 8 ++++---- src/services/purchaseService.ts | 7 +++---- src/services/serversideVendorsService.ts | 20 +++++++++---------- src/types/vendorTypes.ts | 2 +- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts index c7212550..b161176e 100644 --- a/src/controllers/api/getVendorInfoController.ts +++ b/src/controllers/api/getVendorInfoController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; export const getVendorInfoController: RequestHandler = (req, res) => { if (typeof req.query.vendor == "string") { @@ -7,7 +7,7 @@ export const getVendorInfoController: RequestHandler = (req, res) => { if (!manifest) { throw new Error(`Unknown vendor: ${req.query.vendor}`); } - res.json(preprocessVendorManifest(manifest)); + res.json(manifest); } else { res.status(400).end(); } diff --git a/src/controllers/api/postGuildAdvertisementController.ts b/src/controllers/api/postGuildAdvertisementController.ts index e33db645..69b28323 100644 --- a/src/controllers/api/postGuildAdvertisementController.ts +++ b/src/controllers/api/postGuildAdvertisementController.ts @@ -9,7 +9,7 @@ import { } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; import { GuildPermission } from "@/src/types/guildTypes"; import { IPurchaseParams } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; @@ -26,9 +26,9 @@ export const postGuildAdvertisementController: RequestHandler = async (req, res) const payload = getJSONfromString(String(req.body)); // Handle resource cost - const vendor = preprocessVendorManifest( - getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! - ); + const vendor = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest" + )!; const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!; if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) { addVaultMiscItems(guild, [ diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 04525704..32f3af38 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -9,7 +9,7 @@ import { updateSlots } from "@/src/services/inventoryService"; import { getRandomWeightedRewardUc } from "@/src/services/rngService"; -import { getVendorManifestByOid, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; @@ -53,9 +53,8 @@ export const handlePurchase = async ( const prePurchaseInventoryChanges: IInventoryChanges = {}; let seed: bigint | undefined; if (purchaseRequest.PurchaseParams.Source == 7) { - const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); - if (rawManifest) { - const manifest = preprocessVendorManifest(rawManifest); + const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); + if (manifest) { let ItemId: string | undefined; if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 9700e00e..23b382d8 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -3,14 +3,14 @@ import path from "path"; import { repoDir } from "@/src/helpers/pathHelper"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; -import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; +import { IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; import { JSONParse } from "json-with-bigint"; -const getVendorManifestJson = (name: string): IVendorManifest => { +const getVendorManifestJson = (name: string): IRawVendorManifest => { return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); }; -const vendorManifests: IVendorManifest[] = [ +const rawVendorManifests: IRawVendorManifest[] = [ getVendorManifestJson("ArchimedeanVendorManifest"), getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"), getVendorManifestJson("DeimosFishmongerVendorManifest"), @@ -44,25 +44,25 @@ const vendorManifests: IVendorManifest[] = [ getVendorManifestJson("ZarimanCommisionsManifestArchimedean") ]; -export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { - for (const vendorManifest of vendorManifests) { +export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => { + for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { - return vendorManifest; + return preprocessVendorManifest(vendorManifest); } } return undefined; }; -export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { - for (const vendorManifest of vendorManifests) { +export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed | undefined => { + for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo._id.$oid == oid) { - return vendorManifest; + return preprocessVendorManifest(vendorManifest); } } return undefined; }; -export const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifestPreprocessed => { +const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendorManifestPreprocessed => { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { const manifest = structuredClone(originalManifest); const info = manifest.VendorInfo; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index 0aa5b83e..e5c7cdfc 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -38,7 +38,7 @@ interface IVendorInfoPreprocessed extends Omit { ItemManifest: IItemManifestPreprocessed[]; } -export interface IVendorManifest { +export interface IRawVendorManifest { VendorInfo: IVendorInfo; } From 49edebc1eb57a17054c69a54b6441f072e8f6391 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 5 Apr 2025 06:52:35 -0700 Subject: [PATCH 380/776] chore: fix controllers exporting non-RequestHandler things (#1468) I'm surprised JavaScript allows circular includes, but they still don't feel good. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1468 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/claimCompletedRecipeController.ts | 2 +- .../api/getNewRewardSeedController.ts | 7 +- .../giveKeyChainTriggeredItemsController.ts | 8 +- .../giveKeyChainTriggeredMessageController.ts | 2 +- src/controllers/api/giveQuestKey.ts | 4 +- .../api/giveStartingGearController.ts | 85 +------------ .../api/infestedFoundryController.ts | 118 ++---------------- src/controllers/api/inventoryController.ts | 15 +-- src/controllers/api/upgradesController.ts | 2 +- .../getProfileViewingDataController.ts | 2 +- src/helpers/stringHelpers.ts | 10 ++ src/services/infestedFoundryService.ts | 110 ++++++++++++++++ src/services/inventoryService.ts | 94 ++++++++++++-- src/services/itemDataService.ts | 2 +- src/services/missionInventoryUpdateService.ts | 10 +- src/services/questService.ts | 19 ++- src/types/inventoryTypes/inventoryTypes.ts | 2 + src/types/requestTypes.ts | 7 ++ 18 files changed, 243 insertions(+), 256 deletions(-) create mode 100644 src/services/infestedFoundryService.ts diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index f95a950f..880b2267 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -19,7 +19,7 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -export interface IClaimCompletedRecipeRequest { +interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; } diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index dc4cec8e..4ff82405 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,4 +1,5 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { generateRewardSeed } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; @@ -18,9 +19,3 @@ export const getNewRewardSeedController: RequestHandler = async (req, res) => { ); res.json({ rewardSeed: rewardSeed }); }; - -export function generateRewardSeed(): number { - const min = -Number.MAX_SAFE_INTEGER; - const max = Number.MAX_SAFE_INTEGER; - return Math.floor(Math.random() * (max - min + 1)) + min; -} diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts index bff70c85..df8e8a80 100644 --- a/src/controllers/api/giveKeyChainTriggeredItemsController.ts +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -2,8 +2,8 @@ import { RequestHandler } from "express"; import { parseString } from "@/src/helpers/general"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getInventory } from "@/src/services/inventoryService"; -import { IGroup } from "@/src/types/loginTypes"; import { giveKeyChainItem } from "@/src/services/questService"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); @@ -15,9 +15,3 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res.send(inventoryChanges); }; - -export interface IKeyChainRequest { - KeyChain: string; - ChainStage: number; - Groups?: IGroup[]; -} diff --git a/src/controllers/api/giveKeyChainTriggeredMessageController.ts b/src/controllers/api/giveKeyChainTriggeredMessageController.ts index dec4b8a1..3bc41c21 100644 --- a/src/controllers/api/giveKeyChainTriggeredMessageController.ts +++ b/src/controllers/api/giveKeyChainTriggeredMessageController.ts @@ -1,7 +1,7 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { giveKeyChainMessage } from "@/src/services/questService"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { diff --git a/src/controllers/api/giveQuestKey.ts b/src/controllers/api/giveQuestKey.ts index 8cd4b135..80846234 100644 --- a/src/controllers/api/giveQuestKey.ts +++ b/src/controllers/api/giveQuestKey.ts @@ -20,11 +20,11 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) => //TODO: consider whishlist changes }; -export interface IQuestKeyRewardRequest { +interface IQuestKeyRewardRequest { reward: IQuestKeyReward; } -export interface IQuestKeyReward { +interface IQuestKeyReward { RewardType: string; CouponType: string; Icon: string; diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index 74d45e29..6556de93 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -1,20 +1,8 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel"; -import { - addEquipment, - addItem, - addPowerSuit, - combineInventoryChanges, - getInventory, - updateSlots -} from "@/src/services/inventoryService"; +import { addStartingGear, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; -import { HydratedDocument } from "mongoose"; - -type TPartialStartingGear = Pick; export const giveStartingGearController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -26,72 +14,3 @@ export const giveStartingGearController: RequestHandler = async (req, res) => { res.send(inventoryChanges); }; - -//TODO: RawUpgrades might need to return a LastAdded -const awakeningRewards = [ - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1", - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2", - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3", - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4", - "/Lotus/Types/Restoratives/LisetAutoHack", - "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" -]; - -export const addStartingGear = async ( - inventory: HydratedDocument, - startingGear: TPartialStartingGear | undefined = undefined -): Promise => { - const { LongGuns, Pistols, Suits, Melee } = startingGear || { - LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], - Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], - Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }], - Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }] - }; - - //TODO: properly merge weapon bin changes it is currently static here - const inventoryChanges: IInventoryChanges = {}; - addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); - await addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); - addEquipment( - inventory, - "DataKnives", - "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", - undefined, - inventoryChanges, - { XP: 450_000 } - ); - addEquipment( - inventory, - "Scoops", - "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", - undefined, - inventoryChanges - ); - - updateSlots(inventory, InventorySlot.SUITS, 0, 1); - updateSlots(inventory, InventorySlot.WEAPONS, 0, 3); - inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 }; - inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 }; - - await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); - inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"; - - inventory.PremiumCredits = 50; - inventory.PremiumCreditsFree = 50; - inventoryChanges.PremiumCredits = 50; - inventoryChanges.PremiumCreditsFree = 50; - inventory.RegularCredits = 3000; - inventoryChanges.RegularCredits = 3000; - - for (const item of awakeningRewards) { - const inventoryDelta = await addItem(inventory, item); - combineInventoryChanges(inventoryChanges, inventoryDelta); - } - - inventory.PlayedParkourTutorial = true; - inventory.ReceivedStartingGear = true; - - return inventoryChanges; -}; diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index d63c2203..28e89adc 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -6,20 +6,21 @@ import { IOid } from "@/src/types/commonTypes"; import { IConsumedSuit, IHelminthFoodRecord, - IInfestedFoundryClient, - IInfestedFoundryDatabase, IInventoryClient, IMiscItem, - InventorySlot, - ITypeCount + InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -import { ExportMisc, ExportRecipes } from "warframe-public-export-plus"; +import { ExportMisc } from "warframe-public-export-plus"; import { getRecipe } from "@/src/services/itemDataService"; -import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; import { colorToShard } from "@/src/helpers/shardHelper"; import { config } from "@/src/services/configService"; +import { + addInfestedFoundryXP, + applyCheatsToInfestedFoundry, + handleSubsumeCompletion +} from "@/src/services/infestedFoundryService"; export const infestedFoundryController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -383,116 +384,11 @@ interface IHelminthFeedRequest { }[]; } -export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => { - const recipeChanges: ITypeCount[] = []; - infestedFoundry.XP ??= 0; - const prevXP = infestedFoundry.XP; - infestedFoundry.XP += delta; - if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) { - infestedFoundry.Slots ??= 0; - infestedFoundry.Slots += 3; - } - if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) { - recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 }); - } - if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) { - infestedFoundry.Slots ??= 0; - infestedFoundry.Slots += 10; - } - if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) { - recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 }); - } - if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) { - infestedFoundry.Slots ??= 0; - infestedFoundry.Slots += 20; - } - if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) { - recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 }); - } - if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) { - infestedFoundry.Slots = 1; - } - if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint", - ItemCount: 1 - }); - } - return recipeChanges; -}; - interface IHelminthSubsumeRequest { SuitId: IOid; Recipe: string; } -export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => { - const [recipeType] = Object.entries(ExportRecipes).find( - ([_recipeType, recipe]) => - recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" && - recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType - )!; - inventory.InfestedFoundry!.LastConsumedSuit = undefined; - inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined; - const recipeChanges: ITypeCount[] = [ - { - ItemType: recipeType, - ItemCount: 1 - } - ]; - addRecipes(inventory, recipeChanges); - return recipeChanges; -}; - -export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => { - if (config.infiniteHelminthMaterials) { - infestedFoundry.Resources = [ - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 } - ]; - } -}; - interface IHelminthOfferingsUpdate { OfferingsIndex: number; SuitTypes: string[]; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index ab0391a6..2c659717 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -13,9 +13,10 @@ import { ExportResources, ExportVirtuals } from "warframe-public-export-plus"; -import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; +import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; export const inventoryController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -269,7 +270,7 @@ export const getInventoryResponse = async ( return inventoryResponse; }; -export const addString = (arr: string[], str: string): void => { +const addString = (arr: string[], str: string): void => { if (!arr.find(x => x == str)) { arr.push(str); } @@ -299,13 +300,3 @@ const resourceGetParent = (resourceName: string): string | undefined => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return ExportVirtuals[resourceName]?.parentName; }; - -// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere. -export const catBreadHash = (name: string): number => { - let hash = 2166136261; - for (let i = 0; i != name.length; ++i) { - hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff; - hash = (hash * 16777619) & 0x7fffffff; - } - return hash; -}; diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index ae547674..855d2aa7 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -11,7 +11,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getRecipeByResult } from "@/src/services/itemDataService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "./infestedFoundryController"; +import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService"; import { config } from "@/src/services/configService"; export const upgradesController: RequestHandler = async (req, res) => { diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts index c1134604..766e40a2 100644 --- a/src/controllers/dynamic/getProfileViewingDataController.ts +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -18,7 +18,7 @@ import { ITypeXPItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; -import { catBreadHash } from "../api/inventoryController"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; import { IStatsClient } from "@/src/types/statTypes"; import { toStoreItem } from "@/src/services/itemDataService"; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index f24fab32..8925aea2 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -27,6 +27,16 @@ export const getIndexAfter = (str: string, searchWord: string): number => { return index + searchWord.length; }; +// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere. +export const catBreadHash = (name: string): number => { + let hash = 2166136261; + for (let i = 0; i != name.length; ++i) { + hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff; + hash = (hash * 16777619) & 0x7fffffff; + } + return hash; +}; + export const regexEscape = (str: string): string => { str = str.split(".").join("\\."); str = str.split("\\").join("\\\\"); diff --git a/src/services/infestedFoundryService.ts b/src/services/infestedFoundryService.ts new file mode 100644 index 00000000..5afc93fa --- /dev/null +++ b/src/services/infestedFoundryService.ts @@ -0,0 +1,110 @@ +import { ExportRecipes } from "warframe-public-export-plus"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { IInfestedFoundryClient, IInfestedFoundryDatabase, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; +import { addRecipes } from "./inventoryService"; +import { config } from "./configService"; + +export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => { + const recipeChanges: ITypeCount[] = []; + infestedFoundry.XP ??= 0; + const prevXP = infestedFoundry.XP; + infestedFoundry.XP += delta; + if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) { + infestedFoundry.Slots ??= 0; + infestedFoundry.Slots += 3; + } + if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) { + recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 }); + } + if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) { + infestedFoundry.Slots ??= 0; + infestedFoundry.Slots += 10; + } + if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) { + recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 }); + } + if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) { + infestedFoundry.Slots ??= 0; + infestedFoundry.Slots += 20; + } + if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) { + recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 }); + } + if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) { + infestedFoundry.Slots = 1; + } + if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint", + ItemCount: 1 + }); + } + return recipeChanges; +}; + +export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => { + const [recipeType] = Object.entries(ExportRecipes).find( + ([_recipeType, recipe]) => + recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" && + recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType + )!; + inventory.InfestedFoundry!.LastConsumedSuit = undefined; + inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined; + const recipeChanges: ITypeCount[] = [ + { + ItemType: recipeType, + ItemCount: 1 + } + ]; + addRecipes(inventory, recipeChanges); + return recipeChanges; +}; + +export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => { + if (config.infiniteHelminthMaterials) { + infestedFoundry.Resources = [ + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 } + ]; + } +}; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 3e4835da..4a711c6c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1,10 +1,6 @@ -import { - Inventory, - InventoryDocumentProps, - TInventoryDatabaseDocument -} from "@/src/models/inventoryModels/inventoryModel"; +import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; -import { HydratedDocument, Types } from "mongoose"; +import { Types } from "mongoose"; import { SlotNames, IInventoryChanges, IBinChanges, slotNames } from "@/src/types/purchaseTypes"; import { IChallengeProgress, @@ -19,16 +15,16 @@ import { TEquipmentKey, IFusionTreasure, IDailyAffiliations, - IInventoryDatabase, IKubrowPetEggDatabase, IKubrowPetEggClient, ILibraryDailyTaskInfo, ICalendarProgress, IDroneClient, - IUpgradeClient + IUpgradeClient, + TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; -import { IMissionInventoryUpdateRequest } from "../types/requestTypes"; +import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { convertInboxMessage, fromStoreItem, getKeyChainItems } from "@/src/services/itemDataService"; import { @@ -62,10 +58,7 @@ import { TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { toOid } from "../helpers/inventoryHelpers"; -import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; -import { addStartingGear } from "@/src/controllers/api/giveStartingGearController"; import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; @@ -114,6 +107,81 @@ export const createInventory = async ( } }; +export const generateRewardSeed = (): number => { + const min = -Number.MAX_SAFE_INTEGER; + const max = Number.MAX_SAFE_INTEGER; + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +//TODO: RawUpgrades might need to return a LastAdded +const awakeningRewards = [ + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4", + "/Lotus/Types/Restoratives/LisetAutoHack", + "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" +]; + +export const addStartingGear = async ( + inventory: TInventoryDatabaseDocument, + startingGear: TPartialStartingGear | undefined = undefined +): Promise => { + const { LongGuns, Pistols, Suits, Melee } = startingGear || { + LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], + Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], + Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }], + Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }] + }; + + //TODO: properly merge weapon bin changes it is currently static here + const inventoryChanges: IInventoryChanges = {}; + addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); + await addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); + addEquipment( + inventory, + "DataKnives", + "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", + undefined, + inventoryChanges, + { XP: 450_000 } + ); + addEquipment( + inventory, + "Scoops", + "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", + undefined, + inventoryChanges + ); + + updateSlots(inventory, InventorySlot.SUITS, 0, 1); + updateSlots(inventory, InventorySlot.WEAPONS, 0, 3); + inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 }; + inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 }; + + await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); + inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"; + + inventory.PremiumCredits = 50; + inventory.PremiumCreditsFree = 50; + inventoryChanges.PremiumCredits = 50; + inventoryChanges.PremiumCreditsFree = 50; + inventory.RegularCredits = 3000; + inventoryChanges.RegularCredits = 3000; + + for (const item of awakeningRewards) { + const inventoryDelta = await addItem(inventory, item); + combineInventoryChanges(inventoryChanges, inventoryDelta); + } + + inventory.PlayedParkourTutorial = true; + inventory.ReceivedStartingGear = true; + + return inventoryChanges; +}; + /** * Combines two inventory changes objects into one. * @@ -1302,7 +1370,7 @@ export const addBooster = (ItemType: string, time: number, inventory: TInventory }; export const updateSyndicate = ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"] ): void => { syndicateUpdate?.forEach(affiliation => { diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 2fc6c0da..f6feae12 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,4 +1,4 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { getIndexAfter } from "@/src/helpers/stringHelpers"; import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 70ca6eea..60fda2d4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -10,7 +10,7 @@ import { import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; -import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addBooster, addChallenges, @@ -32,10 +32,10 @@ import { updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; -import { HydratedDocument, Types } from "mongoose"; +import { Types } from "mongoose"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; -import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { handleStoreItemAcquisition } from "./purchaseService"; @@ -73,7 +73,7 @@ const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { //const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys export const addMissionInventoryUpdates = async ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, inventoryUpdates: IMissionInventoryUpdateRequest ): Promise => { const inventoryChanges: IInventoryChanges = {}; @@ -661,7 +661,7 @@ interface IMissionCredits { //creditBonus is not entirely accurate. //TODO: consider ActiveBoosters export const addCredits = ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, { missionDropCredits, missionCompletionCredits, diff --git a/src/services/questService.ts b/src/services/questService.ts index 7b82b304..6319a724 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -1,4 +1,4 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { isEmptyObject } from "@/src/helpers/general"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; @@ -9,14 +9,9 @@ import { getLevelKeyRewards, getQuestCompletionItems } from "@/src/services/itemDataService"; -import { - IInventoryDatabase, - IQuestKeyClient, - IQuestKeyDatabase, - IQuestStage -} from "@/src/types/inventoryTypes/inventoryTypes"; +import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; -import { HydratedDocument, Types } from "mongoose"; +import { Types } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; import { addFixedLevelRewards } from "./missionInventoryUpdateService"; import { IInventoryChanges } from "../types/purchaseTypes"; @@ -31,7 +26,7 @@ export interface IUpdateQuestRequest { } export const updateQuestKey = async ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, questKeyUpdate: IUpdateQuestRequest["QuestKeys"] ): Promise => { if (questKeyUpdate.length > 1) { @@ -45,7 +40,7 @@ export const updateQuestKey = async ( throw new Error(`quest key ${questKeyUpdate[0].ItemType} not found`); } - inventory.QuestKeys[questKeyIndex] = questKeyUpdate[0]; + inventory.QuestKeys[questKeyIndex].overwrite(questKeyUpdate[0]); let inventoryChanges: IInventoryChanges = {}; if (questKeyUpdate[0].Completed) { @@ -57,12 +52,12 @@ export const updateQuestKey = async ( logger.debug(`quest completion items`, questCompletionItems); if (questCompletionItems) { - inventoryChanges = await addItems(inventory as TInventoryDatabaseDocument, questCompletionItems); + inventoryChanges = await addItems(inventory, questCompletionItems); } inventory.ActiveQuest = ""; if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { - setupKahlSyndicate(inventory as TInventoryDatabaseDocument); + setupKahlSyndicate(inventory); } } return inventoryChanges; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 4eee21b6..c25b7123 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1168,3 +1168,5 @@ export interface ILockedWeaponGroupDatabase { m?: Types.ObjectId; sn?: Types.ObjectId; } + +export type TPartialStartingGear = Pick; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 7c14f696..d9139ec7 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -21,6 +21,7 @@ import { ILockedWeaponGroupClient, ILoadOutPresets } from "./inventoryTypes/inventoryTypes"; +import { IGroup } from "./loginTypes"; export interface IAffiliationChange { Tag: string; @@ -175,3 +176,9 @@ export interface IVoidTearParticipantInfo { RewardProjection: string; HardModeReward: ITypeCount; } + +export interface IKeyChainRequest { + KeyChain: string; + ChainStage: number; + Groups?: IGroup[]; +} From 65306e0478c470cb2878f74050ef57189bdafcdd Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sat, 5 Apr 2025 07:45:10 -0700 Subject: [PATCH 381/776] chore(webui): update to German translation (#1469) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1469 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index f04d2780..ba6a405a 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -45,14 +45,14 @@ dict = { code_zanukaA: `Jagdhund: Dorma`, code_zanukaB: `Jagdhund: Bhaira`, code_zanukaC: `Jagdhund: Hec`, - code_stage: `[UNTRANSLATED] Stage`, - code_complete: `[UNTRANSLATED] Complete`, - code_nextStage: `[UNTRANSLATED] Next stage`, - code_prevStage: `[UNTRANSLATED] Previous stage`, - code_reset: `[UNTRANSLATED] Reset`, - code_setInactive: `[UNTRANSLATED] Make the quest inactive`, - code_completed: `[UNTRANSLATED] Completed`, - code_active: `[UNTRANSLATED] Active`, + code_stage: `Abschnitt`, + code_complete: `Abschließen`, + code_nextStage: `Nächster Abschnitt`, + code_prevStage: `Vorheriger Abschnitt`, + code_reset: `Zurücksetzen`, + code_setInactive: `Quest inaktiv setzen`, + code_completed: `Abgeschlossen`, + code_active: `Aktiv`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, From 3c79f910a28c1a555ed323bde98f17d3ff385fc7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:03:55 -0700 Subject: [PATCH 382/776] feat: coda weapon vendor rotation (#1471) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1471 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 49 +++++- src/types/vendorTypes.ts | 6 +- .../InfestedLichWeaponVendorManifest.json | 157 ------------------ 3 files changed, 52 insertions(+), 160 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 23b382d8..961b5faa 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -3,8 +3,9 @@ import path from "path"; import { repoDir } from "@/src/helpers/pathHelper"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; -import { IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; +import { IItemManifestPreprocessed, IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; import { JSONParse } from "json-with-bigint"; +import { ExportVendors } from "warframe-public-export-plus"; const getVendorManifestJson = (name: string): IRawVendorManifest => { return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); @@ -29,7 +30,6 @@ const rawVendorManifests: IRawVendorManifest[] = [ getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"), getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), - getVendorManifestJson("InfestedLichWeaponVendorManifest"), getVendorManifestJson("MaskSalesmanManifest"), getVendorManifestJson("Nova1999ConquestShopManifest"), getVendorManifestJson("OstronFishmongerVendorManifest"), @@ -50,6 +50,9 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPr return preprocessVendorManifest(vendorManifest); } } + if (typeName == "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest") { + return generateCodaWeaponVendorManifest(); + } return undefined; }; @@ -59,6 +62,9 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed return preprocessVendorManifest(vendorManifest); } } + if (oid == "67dadc30e4b6e0e5979c8d84") { + return generateCodaWeaponVendorManifest(); + } return undefined; }; @@ -96,3 +102,42 @@ const refreshExpiry = (expiry: IMongoDate): number => { } return 0; }; + +const generateCodaWeaponVendorManifest = (): IVendorManifestPreprocessed => { + const EPOCH = 1740960000 * 1000; + const DUR = 4 * 86400 * 1000; + const cycle = Math.trunc((Date.now() - EPOCH) / DUR); + const cycleStart = EPOCH + cycle * DUR; + const cycleEnd = cycleStart + DUR; + const binThisCycle = cycle % 2; // isOneBinPerCycle + const items: IItemManifestPreprocessed[] = []; + const manifest = ExportVendors["/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest"]; + const rng = new CRng(cycle); + for (const rawItem of manifest.items) { + if (rawItem.bin != binThisCycle) { + continue; + } + items.push({ + StoreItem: rawItem.storeItem, + ItemPrices: rawItem.itemPrices!.map(item => ({ ...item, ProductCategory: "MiscItems" })), + Bin: "BIN_" + rawItem.bin, + QuantityMultiplier: 1, + Expiry: { $date: { $numberLong: cycleEnd.toString() } }, + AllowMultipurchase: false, + LocTagRandSeed: (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)), + Id: { $oid: "67e9da12793a120d" + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") } + }); + } + return { + VendorInfo: { + _id: { $oid: "67dadc30e4b6e0e5979c8d84" }, + TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + ItemManifest: items, + PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56", + RandomSeedType: "VRST_WEAPON", + RequiredGoalTag: "", + WeaponUpgradeValueAttenuationExponent: 2.25, + Expiry: { $date: { $numberLong: cycleEnd.toString() } } + } + }; +}; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index e5c7cdfc..f962a494 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -23,7 +23,7 @@ interface IItemManifest { Id: IOid; } -interface IItemManifestPreprocessed extends Omit { +export interface IItemManifestPreprocessed extends Omit { ItemPrices?: IItemPricePreprocessed[]; } @@ -31,6 +31,10 @@ interface IVendorInfo { _id: IOid; TypeName: string; ItemManifest: IItemManifest[]; + PropertyTextHash?: string; + RandomSeedType?: "VRST_WEAPON"; + RequiredGoalTag?: string; + WeaponUpgradeValueAttenuationExponent?: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. } diff --git a/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json deleted file mode 100644 index 04a0392d..00000000 --- a/static/fixed_responses/getVendorInfo/InfestedLichWeaponVendorManifest.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "67dadc30e4b6e0e5979c8d84" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/LongGuns/1999InfShotgun/1999InfShotgunWeapon", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 65079176837546984, - "Id": { - "$oid": "67e9da12793a120dbbc1c193" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaCaustacyst/CodaCaustacyst", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 5687904240491804000, - "Id": { - "$oid": "67e9da12793a120dbbc1c194" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaPathocyst/CodaPathocyst", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 6177144662234093000, - "Id": { - "$oid": "67e9da12793a120dbbc1c195" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Pistols/CodaTysis", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 1988275604378227700, - "Id": { - "$oid": "67e9da12793a120dbbc1c196" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/LongGuns/CodaSynapse", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 8607452585593957000, - "Id": { - "$oid": "67e9da12793a120dbbc1c197" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Infested/InfestedLich/Melee/CodaHirudo", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 8385013066220909000, - "Id": { - "$oid": "67e9da12793a120dbbc1c198" - } - } - ], - "PropertyTextHash": "77093DD05A8561A022DEC9A4B9BB4A56", - "RandomSeedType": "VRST_WEAPON", - "RequiredGoalTag": "", - "WeaponUpgradeValueAttenuationExponent": 2.25, - "Expiry": { - "$date": { - "$numberLong": "9999999999999" - } - } - } -} From 2bdb722986b1b9f0487b0e1e56e4bb7f05fd0bb2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:04:04 -0700 Subject: [PATCH 383/776] fix: invalid format in inventory response for UpgradesExpiry (#1473) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1473 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 01c4dbc0..a142273e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -899,6 +899,9 @@ EquipmentSchema.set("toJSON", { if (db.InfestationDate) { client.InfestationDate = toMongoDate(db.InfestationDate); } + if (db.UpgradesExpiry) { + client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry); + } } }); From 5f6b2330afc82f478ed5bd4f0b287c9ab3626c12 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:04:16 -0700 Subject: [PATCH 384/776] chore: remove /Lotus/Types/Recipes/ from path-based logic (#1475) Both recipes & MiscItems (recipe components) start with this. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1475 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 4a711c6c..9a841453 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -673,17 +673,6 @@ export const addItem = async ( Horses: [inventory.Horses[horseIndex - 1].toJSON()] }; } - case "Recipes": { - inventory.MiscItems.push({ ItemType: typeName, ItemCount: quantity }); - return { - MiscItems: [ - { - ItemType: typeName, - ItemCount: quantity - } - ] - }; - } case "Vehicles": if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") { return addMotorcycle(inventory, typeName); From b2497ded19debfab337a24a1b9ceae177ebcd0d5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:04:30 -0700 Subject: [PATCH 385/776] fix: refuse to add items non-fatally (#1476) This is needed to complete to the railjack quest when already owning a railjack Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1476 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9a841453..904c90fa 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1060,11 +1060,12 @@ const addCrewShip = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { if (inventory.CrewShips.length != 0) { - throw new Error("refusing to add CrewShip because account already has one"); + logger.warn("refusing to add CrewShip because account already has one"); + } else { + const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; + inventoryChanges.CrewShips ??= []; + inventoryChanges.CrewShips.push(inventory.CrewShips[index].toJSON()); } - const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; - inventoryChanges.CrewShips ??= []; - inventoryChanges.CrewShips.push(inventory.CrewShips[index].toJSON()); return inventoryChanges; }; @@ -1074,11 +1075,12 @@ const addCrewShipHarness = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { if (inventory.CrewShipHarnesses.length != 0) { - throw new Error("refusing to add CrewShipHarness because account already has one"); + logger.warn("refusing to add CrewShipHarness because account already has one"); + } else { + const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; + inventoryChanges.CrewShipHarnesses ??= []; + inventoryChanges.CrewShipHarnesses.push(inventory.CrewShipHarnesses[index].toJSON()); } - const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; - inventoryChanges.CrewShipHarnesses ??= []; - inventoryChanges.CrewShipHarnesses.push(inventory.CrewShipHarnesses[index].toJSON()); return inventoryChanges; }; @@ -1088,11 +1090,12 @@ const addMotorcycle = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { if (inventory.Motorcycles.length != 0) { - throw new Error("refusing to add Motorcycle because account already has one"); + logger.warn("refusing to add Motorcycle because account already has one"); + } else { + const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; + inventoryChanges.Motorcycles ??= []; + inventoryChanges.Motorcycles.push(inventory.Motorcycles[index].toJSON()); } - const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; - inventoryChanges.Motorcycles ??= []; - inventoryChanges.Motorcycles.push(inventory.Motorcycles[index].toJSON()); return inventoryChanges; }; From 64da8c2e50e0b9b20d1eb7f401414acfe09214af Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:04:44 -0700 Subject: [PATCH 386/776] feat: no mastery rank up cooldown cheat (#1478) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1478 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + src/controllers/api/trainingResultController.ts | 8 +++++++- src/services/configService.ts | 1 + static/webui/index.html | 4 ++++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 9 files changed, 18 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index d1c93a66..4e52c750 100644 --- a/config.json.example +++ b/config.json.example @@ -30,6 +30,7 @@ "unlockArcanesEverywhere": false, "noDailyStandingLimits": false, "noArgonCrystalDecay": false, + "noMasteryRankUpCooldown": false, "noVendorPurchaseLimits": true, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": false, diff --git a/src/controllers/api/trainingResultController.ts b/src/controllers/api/trainingResultController.ts index a9bc196e..639f0db6 100644 --- a/src/controllers/api/trainingResultController.ts +++ b/src/controllers/api/trainingResultController.ts @@ -6,6 +6,7 @@ import { RequestHandler } from "express"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { createMessage } from "@/src/services/inboxService"; +import { config } from "@/src/services/configService"; interface ITrainingResultsRequest { numLevelsGained: number; @@ -25,7 +26,12 @@ const trainingResultController: RequestHandler = async (req, res): Promise const inventory = await getInventory(accountId); if (trainingResults.numLevelsGained == 1) { - inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23); + let time = Date.now(); + if (!config.noMasteryRankUpCooldown) { + time += unixTimesInMs.hour * 23; + } + inventory.TrainingDate = new Date(time); + inventory.PlayerLevel += 1; await createMessage(accountId, [ diff --git a/src/services/configService.ts b/src/services/configService.ts index c01fee7e..04c47d36 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -35,6 +35,7 @@ interface IConfig { unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; noArgonCrystalDecay?: boolean; + noMasteryRankUpCooldown?: boolean; noVendorPurchaseLimits?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; diff --git a/static/webui/index.html b/static/webui/index.html index 74ea23d6..6468d53a 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -592,6 +592,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index ba6a405a..49fcb1cd 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -132,6 +132,7 @@ dict = { cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index ba1e406c..798c69db 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -131,6 +131,7 @@ dict = { cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, + cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 9e307117..eb635c5a 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -132,6 +132,7 @@ dict = { cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 294b526e..893a864d 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -132,6 +132,7 @@ dict = { cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 5ec64622..29890fd0 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -132,6 +132,7 @@ dict = { cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, cheats_noDailyStandingLimits: `无每日声望限制`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, From b93a4a6dae15e29d872b04c75b3f921af84d4b33 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:04:55 -0700 Subject: [PATCH 387/776] fix: handle login reward not being able to give any recipe (#1479) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1479 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/loginRewardService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index 7bfc0d71..2f42f04b 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -99,6 +99,10 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab eligibleRecipes.push(uniqueName); } } + if (eligibleRecipes.length == 0) { + // This account has all warframes and weapons already mastered (filthy cheater), need a different reward. + return getRandomLoginReward(rng, day, inventory); + } reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)); } return { From ea6facf3fcd94315584d50a5b8cf34cd25448972 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sun, 6 Apr 2025 06:44:11 -0700 Subject: [PATCH 388/776] chore(webui): update to German translation (#1490) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1490 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 49fcb1cd..1fffac3a 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -132,7 +132,7 @@ dict = { cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, - cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, + cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, From f906cdb5e895ae77acef6158bd180a1125c01b25 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:18:01 -0700 Subject: [PATCH 389/776] fix: handle client providing an invalid loadout id at EOM upload (#1486) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1486 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 60fda2d4..0f7b5e55 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -441,7 +441,12 @@ export const addMissionInventoryUpdates = async ( _id: new Types.ObjectId(ItemId.$oid), ...loadoutConfigItemIdRemoved }; - loadout.NORMAL.id(loadoutId)!.overwrite(loadoutConfigDatabase); + const dbConfig = loadout.NORMAL.id(loadoutId); + if (dbConfig) { + dbConfig.overwrite(loadoutConfigDatabase); + } else { + logger.warn(`couldn't update loadout because there's no config with id ${loadoutId}`); + } } await loadout.save(); } From 8f41d3c13fcefb83960d2a640e8254929ea8d9c4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:18:15 -0700 Subject: [PATCH 390/776] fix: give an extra trade when leveling up MR (#1487) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1487 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/trainingResultController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/trainingResultController.ts b/src/controllers/api/trainingResultController.ts index 639f0db6..f44d7dae 100644 --- a/src/controllers/api/trainingResultController.ts +++ b/src/controllers/api/trainingResultController.ts @@ -23,7 +23,7 @@ const trainingResultController: RequestHandler = async (req, res): Promise const trainingResults = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining"); if (trainingResults.numLevelsGained == 1) { let time = Date.now(); @@ -33,6 +33,7 @@ const trainingResultController: RequestHandler = async (req, res): Promise inventory.TrainingDate = new Date(time); inventory.PlayerLevel += 1; + inventory.TradesRemaining += 1; await createMessage(accountId, [ { From fe0b745066d7640846df421ad84af04e52adca89 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:18:33 -0700 Subject: [PATCH 391/776] fix: missing fields in dojo response (#1488) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1488 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/guildService.ts | 7 +++++-- src/types/guildTypes.ts | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 2c7d683f..2f229d1f 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -95,7 +95,6 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s LongMOTD: guild.LongMOTD, Members: members, Ranks: guild.Ranks, - TradeTax: guild.TradeTax, Tier: guild.Tier, Vault: getGuildVault(guild), ActiveDojoColorResearch: guild.ActiveDojoColorResearch, @@ -126,7 +125,11 @@ export const getDojoClient = async ( const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, Name: guild.Name, - Tier: 1, + Tier: guild.Tier, + GuildEmblem: guild.Emblem, + TradeTax: guild.TradeTax, + NumContributors: guild.CeremonyContributors?.length ?? 0, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, FixedContributions: true, DojoRevision: 1, Vault: getGuildVault(guild), diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index d7d6d66d..8af68809 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -10,7 +10,6 @@ export interface IGuildClient { LongMOTD?: ILongMOTD; Members: IGuildMemberClient[]; Ranks: IGuildRank[]; - TradeTax: number; Tier: number; Vault: IGuildVault; ActiveDojoColorResearch: string; @@ -143,6 +142,7 @@ export interface IDojoClient { _id: IOid; // ID of the guild Name: string; Tier: number; + TradeTax?: number; FixedContributions: boolean; DojoRevision: number; AllianceId?: IOid; @@ -155,6 +155,8 @@ export interface IDojoClient { ContentURL?: string; GuildEmblem?: boolean; DojoComponents: IDojoComponentClient[]; + NumContributors?: number; + CeremonyResetDate?: IMongoDate; } export interface IDojoComponentClient { From ceb7deec06b7b28aa9c5cb5d59d475246b3c48f3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:18:50 -0700 Subject: [PATCH 392/776] chore: generate source maps with build (#1489) This ensures that when we get a stack trace, it contains the original line numbers. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1489 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36bf7738..aece47ca 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", - "build": "tsc --incremental && ncp static/webui build/static/webui", + "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "verify": "tsgo --noEmit", "lint": "eslint --ext .ts .", "lint:fix": "eslint --fix --ext .ts .", From 9698baa979e03341ec1d3456944b5e5e043de2ac Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:19:15 -0700 Subject: [PATCH 393/776] feat: handle droptable rewards from level key (#1492) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1492 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/missionInventoryUpdateService.ts | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 890754cc..c036a710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.50", + "warframe-public-export-plus": "^0.5.51", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.50", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.50.tgz", - "integrity": "sha512-KlhdY/Q5sRAIn/RhmdviKBoX3gk+Jtuen0cWnFB2zqK7eKYMDtd79bKOtTPtnK9zCNzh6gFug2wEeDVam3Bwlw==" + "version": "0.5.51", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.51.tgz", + "integrity": "sha512-V1mMf3Q9711fBtE2LbbliGTemYIxMuuKlCOnv4juttKppVXI/e4zPNpVo/eSvTbqTP7RMm/WPsooOUxn42si7Q==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index aece47ca..0113efe0 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.50", + "warframe-public-export-plus": "^0.5.51", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 0f7b5e55..54620a1a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -726,6 +726,20 @@ export const addFixedLevelRewards = ( MissionRewards.push(item); } } + if (rewards.droptable) { + if (rewards.droptable in ExportRewards) { + logger.debug(`rolling ${rewards.droptable} for level key rewards`); + const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][0]); + if (reward) { + MissionRewards.push({ + StoreItem: reward.type, + ItemCount: reward.itemCount + }); + } + } else { + logger.error(`unknown droptable ${rewards.droptable}`); + } + } return missionBonusCredits; }; From 2ff535e7ab0595645b707314e06bcafbea791cd6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 21:19:24 +0200 Subject: [PATCH 394/776] chore: update PE+ --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c036a710..c1fe1acd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.51", + "warframe-public-export-plus": "^0.5.52", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.51", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.51.tgz", - "integrity": "sha512-V1mMf3Q9711fBtE2LbbliGTemYIxMuuKlCOnv4juttKppVXI/e4zPNpVo/eSvTbqTP7RMm/WPsooOUxn42si7Q==" + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.52.tgz", + "integrity": "sha512-mJyQbTFMDwgBSkhUYJzcfJg9qrMTrL1pyZuAxV/Dov68xUikK5zigQSYM3ZkKYbhwBtg0Bx/+7q9GAmPzGaRhA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 0113efe0..3c0c536d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.51", + "warframe-public-export-plus": "^0.5.52", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" From fac52bfda18f6af9d16167d211684bfb77447260 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 23:19:00 +0200 Subject: [PATCH 395/776] fix: scale credits subtracted from clan vault when auto-contributing --- src/controllers/api/placeDecoInComponentController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index deaefc06..ea40dae3 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -77,8 +77,8 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = } } if (enoughMiscItems) { - guild.VaultRegularCredits -= meta.price; - deco.RegularCredits = meta.price; + guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price); + deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); deco.MiscItems = []; for (const ingredient of meta.ingredients) { From 5702ab5f3bc798c4523f1fac10992196328d5a7f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 23:21:43 +0200 Subject: [PATCH 396/776] fix: missing AutoContributeFromVault in guild response --- src/services/guildService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 2f229d1f..6388acef 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -103,6 +103,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), NumContributors: guild.CeremonyContributors?.length ?? 0, CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, + AutoContributeFromVault: guild.AutoContributeFromVault, AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined }; }; From 4362a842ff39f1b1f4cc740cb1523d63e87d1485 Mon Sep 17 00:00:00 2001 From: VampireKitten Date: Sun, 6 Apr 2025 16:05:47 -0700 Subject: [PATCH 397/776] Fix the rewards of Second Dream (#1498) Adds the two missing inbox items of Second Dream. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1498 Co-authored-by: VampireKitten Co-committed-by: VampireKitten --- src/services/questService.ts | 16 ++++++++++++++++ .../fixed_responses/questCompletionRewards.json | 8 +++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/services/questService.ts b/src/services/questService.ts index 6319a724..e738eb57 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -177,6 +177,22 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); } + if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") { + void createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage", + att: [ + "/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword", + "/Lotus/Upgrades/Skins/Sigils/ScarSigil" + ], + sub: "/Lotus/Language/G1Quests/SecondDreamFinishInboxTitle", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + highPriority: true + } + ]); + } + const questCompletionItems = getQuestCompletionItems(questKey); logger.debug(`quest completion items`, questCompletionItems); if (questCompletionItems) { diff --git a/static/fixed_responses/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json index d0f692aa..71fb423e 100644 --- a/static/fixed_responses/questCompletionRewards.json +++ b/static/fixed_responses/questCompletionRewards.json @@ -5,5 +5,11 @@ "ItemCount": 1 } ], - "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain": [{ "ItemType": "/Lotus/Types/Recipes/WarframeRecipes/BrokenFrameBlueprint", "ItemCount": 1 }] + "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain": [{ "ItemType": "/Lotus/Types/Recipes/WarframeRecipes/BrokenFrameBlueprint", "ItemCount": 1 }], + "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain": [ + { + "ItemType": "/Lotus/Types/Keys/RailJackBuildQuest/RailjackBuildQuestEmailItem", + "ItemCount": 1 + } + ] } From 002b0cb93f4533b5baa5aa864ba50f0b2600e049 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Apr 2025 16:08:36 -0700 Subject: [PATCH 398/776] chore: fix code duplication for quest completion (#1497) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1497 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/itemDataService.ts | 29 -------------- src/services/questService.ts | 71 +++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 55 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index f6feae12..e071111f 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,7 +1,5 @@ import { IKeyChainRequest } from "@/src/types/requestTypes"; import { getIndexAfter } from "@/src/helpers/stringHelpers"; -import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; -import { logger } from "@/src/utils/logger"; import { dict_de, dict_en, @@ -34,7 +32,6 @@ import { IRecipe, TReward } from "warframe-public-export-plus"; -import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; import { IMessage } from "../models/inboxModel"; export type WeaponTypeInternal = @@ -181,32 +178,6 @@ export const getLevelKeyRewards = ( }; }; -export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { - if (questKey in questCompletionItems) { - return questCompletionItems[questKey as keyof typeof questCompletionItems]; - } - logger.warn(`Quest ${questKey} not found in questCompletionItems`); - - const items: ITypeCount[] = []; - const meta = ExportKeys[questKey]; - if (meta.rewards) { - for (const reward of meta.rewards) { - if (reward.rewardType == "RT_STORE_ITEM") { - items.push({ - ItemType: fromStoreItem(reward.itemType), - ItemCount: 1 - }); - } else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") { - items.push({ - ItemType: reward.itemType, - ItemCount: reward.amount - }); - } - } - } - return items; -}; - export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IMessage => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[KeyChain]?.chainStages; diff --git a/src/services/questService.ts b/src/services/questService.ts index e738eb57..78b3aba0 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -3,18 +3,14 @@ import { isEmptyObject } from "@/src/helpers/general"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "@/src/services/inventoryService"; -import { - fromStoreItem, - getKeyChainMessage, - getLevelKeyRewards, - getQuestCompletionItems -} from "@/src/services/itemDataService"; -import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; +import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; +import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { Types } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; import { addFixedLevelRewards } from "./missionInventoryUpdateService"; import { IInventoryChanges } from "../types/purchaseTypes"; +import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; export interface IUpdateQuestRequest { QuestKeys: Omit[]; @@ -42,23 +38,12 @@ export const updateQuestKey = async ( inventory.QuestKeys[questKeyIndex].overwrite(questKeyUpdate[0]); - let inventoryChanges: IInventoryChanges = {}; + const inventoryChanges: IInventoryChanges = {}; if (questKeyUpdate[0].Completed) { inventory.QuestKeys[questKeyIndex].CompletionDate = new Date(); - logger.debug(`completed quest ${questKeyUpdate[0].ItemType} `); - const questKeyName = questKeyUpdate[0].ItemType; - const questCompletionItems = getQuestCompletionItems(questKeyName); - logger.debug(`quest completion items`, questCompletionItems); - - if (questCompletionItems) { - inventoryChanges = await addItems(inventory, questCompletionItems); - } - inventory.ActiveQuest = ""; - - if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { - setupKahlSyndicate(inventory); - } + const questKey = questKeyUpdate[0].ItemType; + await handleQuestCompletion(inventory, questKey, inventoryChanges); } return inventoryChanges; }; @@ -177,6 +162,42 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); } + await handleQuestCompletion(inventory, questKey); +}; + +const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { + if (questKey in questCompletionItems) { + return questCompletionItems[questKey as keyof typeof questCompletionItems]; + } + logger.warn(`Quest ${questKey} not found in questCompletionItems`); + + const items: ITypeCount[] = []; + const meta = ExportKeys[questKey]; + if (meta.rewards) { + for (const reward of meta.rewards) { + if (reward.rewardType == "RT_STORE_ITEM") { + items.push({ + ItemType: fromStoreItem(reward.itemType), + ItemCount: 1 + }); + } else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") { + items.push({ + ItemType: reward.itemType, + ItemCount: reward.amount + }); + } + } + } + return items; +}; + +const handleQuestCompletion = async ( + inventory: TInventoryDatabaseDocument, + questKey: string, + inventoryChanges: IInventoryChanges = {} +): Promise => { + logger.debug(`completed quest ${questKey}`); + if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") { void createMessage(inventory.accountOwnerId, [ { @@ -191,19 +212,17 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest highPriority: true } ]); + } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + setupKahlSyndicate(inventory); } const questCompletionItems = getQuestCompletionItems(questKey); logger.debug(`quest completion items`, questCompletionItems); if (questCompletionItems) { - await addItems(inventory, questCompletionItems); + await addItems(inventory, questCompletionItems, inventoryChanges); } if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = ""; - - if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { - setupKahlSyndicate(inventory); - } }; export const giveKeyChainItem = async ( From 94993a16aaaae0358d9ef7f85bc8d79feec9ac9e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 7 Apr 2025 01:13:47 +0200 Subject: [PATCH 399/776] fix: use await instead of void --- 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 78b3aba0..bae74f66 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -199,7 +199,7 @@ const handleQuestCompletion = async ( logger.debug(`completed quest ${questKey}`); if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") { - void createMessage(inventory.accountOwnerId, [ + await createMessage(inventory.accountOwnerId, [ { sndr: "/Lotus/Language/Bosses/Ordis", msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage", From 919f12b8f93b086d9a5574663f255a3d0a996b84 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:29:21 -0700 Subject: [PATCH 400/776] feat: sortie rotation (#1453) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1453 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../dynamic/worldStateController.ts | 164 +++++++++++++++++- src/helpers/worlstateHelper.ts | 130 ++++++++++++++ .../worldState/worldState.json | 17 -- 3 files changed, 292 insertions(+), 19 deletions(-) create mode 100644 src/helpers/worlstateHelper.ts diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 05c7f60f..3cf397ba 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -10,8 +10,16 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { ExportNightwave, ExportRegions } from "warframe-public-export-plus"; - -const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 +import { + EPOCH, + getSortieTime, + missionTags, + sortieBosses, + sortieBossNode, + sortieBossToFaction, + sortieFactionToFactionIndexes, + sortieFactionToSystemIndexes +} from "@/src/helpers/worlstateHelper"; export const worldStateController: RequestHandler = (req, res) => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); @@ -27,6 +35,7 @@ export const worldStateController: RequestHandler = (req, res) => { Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), Goals: [], GlobalUpgrades: [], + Sorties: [], LiteSorties: [], EndlessXpChoices: [], SeasonInfo: { @@ -154,6 +163,142 @@ export const worldStateController: RequestHandler = (req, res) => { }); } + // Sortie cycling every day + { + let genDay; + let dayStart; + let dayEnd; + const sortieRolloverToday = getSortieTime(day); + if (Date.now() < sortieRolloverToday) { + // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`. + genDay = day - 1; + dayStart = getSortieTime(genDay); + dayEnd = sortieRolloverToday; + } else { + // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`. + genDay = day; + dayStart = sortieRolloverToday; + dayEnd = getSortieTime(day + 1); + } + + const rng = new CRng(genDay); + + const boss = rng.randomElement(sortieBosses); + + const modifiers = [ + "SORTIE_MODIFIER_LOW_ENERGY", + "SORTIE_MODIFIER_IMPACT", + "SORTIE_MODIFIER_SLASH", + "SORTIE_MODIFIER_PUNCTURE", + "SORTIE_MODIFIER_EXIMUS", + "SORTIE_MODIFIER_MAGNETIC", + "SORTIE_MODIFIER_CORROSIVE", + "SORTIE_MODIFIER_VIRAL", + "SORTIE_MODIFIER_ELECTRICITY", + "SORTIE_MODIFIER_RADIATION", + "SORTIE_MODIFIER_GAS", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_TOXIN", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_HAZARD_RADIATION", + "SORTIE_MODIFIER_HAZARD_MAGNETIC", + "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest + "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon + "SORTIE_MODIFIER_HAZARD_ICE", + "SORTIE_MODIFIER_HAZARD_COLD", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_MELEE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + + if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); + if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); + + const nodes: string[] = []; + const availableMissionIndexes: number[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && + sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 5 && // Exclude MT_CAPTURE + value.missionIndex != 21 && // Exclude MT_PURIFY + value.missionIndex != 23 && // Exclude MT_JUNCTION + value.missionIndex <= 28 + ) { + if (!availableMissionIndexes.includes(value.missionIndex)) { + availableMissionIndexes.push(value.missionIndex); + } + nodes.push(key); + } + } + + const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; + const missionTypes = new Set(); + + for (let i = 0; i < 3; i++) { + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; + let missionIndex = ExportRegions[node].missionIndex; + + if ( + !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions + missionIndex != 28 && + rng.randomInt(0, 2) == 2 + ) { + missionIndex = rng.randomElement(availableMissionIndexes); + } + + if (i == 2 && rng.randomInt(0, 2) == 2) { + const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); + const modifierType = rng.randomElement(filteredModifiers); + + if (boss == "SORTIE_BOSS_PHORID") { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); + nodes.splice(randomIndex, 1); + continue; + } else if (sortieBossNode[boss]) { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); + continue; + } + } + + const missionType = missionTags[missionIndex]; + + if (missionTypes.has(missionType)) { + i--; + continue; + } + + const filteredModifiers = + missionType === "MT_TERRITORY" + ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") + : modifiers; + + const modifierType = rng.randomElement(filteredModifiers); + + selectedNodes.push({ missionType, modifierType, node }); + nodes.splice(randomIndex, 1); + missionTypes.add(missionType); + } + + worldState.Sorties.push({ + _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", + Seed: genDay, + Boss: boss, + Variants: selectedNodes + }); + } + // Archon Hunt cycling every week { const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; @@ -298,6 +443,7 @@ interface IWorldState { Goals: IGoal[]; SyndicateMissions: ISyndicateMission[]; GlobalUpgrades: IGlobalUpgrade[]; + Sorties: ISortie[]; LiteSorties: ILiteSortie[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; @@ -361,6 +507,20 @@ interface INodeOverride { CustomNpcEncounters?: string; } +interface ISortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards"; + Seed: number; + Boss: string; + Variants: { + missionType: string; + modifierType: string; + node: string; + }[]; +} + interface ILiteSortie { _id: IOid; Activation: IMongoDate; diff --git a/src/helpers/worlstateHelper.ts b/src/helpers/worlstateHelper.ts new file mode 100644 index 00000000..4501d4d1 --- /dev/null +++ b/src/helpers/worlstateHelper.ts @@ -0,0 +1,130 @@ +export const missionTags = [ + "MT_ASSASSINATION", + "MT_EXTERMINATION", + "MT_SURVIVAL", + "MT_RESCUE", + "MT_SABOTAGE", + "MT_CAPTURE", + "MT_COUNTER_INTEL", + "MT_INTEL", + "MT_DEFENSE", + "MT_MOBILE_DEFENSE", + "MT_PVP", + "MT_MASTERY", + "MT_RECOVERY", + "MT_TERRITORY", + "MT_RETRIEVAL", + "MT_HIVE", + "MT_SALVAGE", + "MT_EXCAVATE", + "MT_RAID", + "MT_PURGE", + "MT_GENERIC", + "MT_PURIFY", + "MT_ARENA", + "MT_JUNCTION", + "MT_PURSUIT", + "MT_RACE", + "MT_ASSAULT", + "MT_EVACUATION", + "MT_LANDSCAPE", + "MT_RESOURCE_THEFT", + "MT_ENDLESS_EXTERMINATION", + "MT_ENDLESS_DUVIRI", + "MT_RAILJACK", + "MT_ARTIFACT", + "MT_CORRUPTION", + "MT_VOID_CASCADE", + "MT_ARMAGEDDON", + "MT_VAULTS", + "MT_ALCHEMY", + "MT_ASCENSION", + "MT_ENDLESS_CAPTURE", + "MT_OFFERING", + "MT_PVPVE" +]; + +export const sortieBosses = [ + "SORTIE_BOSS_HYENA", + "SORTIE_BOSS_KELA", + "SORTIE_BOSS_VOR", + "SORTIE_BOSS_RUK", + "SORTIE_BOSS_HEK", + "SORTIE_BOSS_KRIL", + "SORTIE_BOSS_TYL", + "SORTIE_BOSS_JACKAL", + "SORTIE_BOSS_ALAD", + "SORTIE_BOSS_AMBULAS", + "SORTIE_BOSS_NEF", + "SORTIE_BOSS_RAPTOR", + "SORTIE_BOSS_PHORID", + "SORTIE_BOSS_LEPHANTIS", + "SORTIE_BOSS_INFALAD", + "SORTIE_BOSS_CORRUPTED_VOR" +]; + +export const sortieBossToFaction: Record = { + SORTIE_BOSS_HYENA: "FC_CORPUS", + SORTIE_BOSS_KELA: "FC_GRINEER", + SORTIE_BOSS_VOR: "FC_GRINEER", + SORTIE_BOSS_RUK: "FC_GRINEER", + SORTIE_BOSS_HEK: "FC_GRINEER", + SORTIE_BOSS_KRIL: "FC_GRINEER", + SORTIE_BOSS_TYL: "FC_GRINEER", + SORTIE_BOSS_JACKAL: "FC_CORPUS", + SORTIE_BOSS_ALAD: "FC_CORPUS", + SORTIE_BOSS_AMBULAS: "FC_CORPUS", + SORTIE_BOSS_NEF: "FC_CORPUS", + SORTIE_BOSS_RAPTOR: "FC_CORPUS", + SORTIE_BOSS_PHORID: "FC_INFESTATION", + SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION", + SORTIE_BOSS_INFALAD: "FC_INFESTATION", + SORTIE_BOSS_CORRUPTED_VOR: "FC_CORRUPTED" +}; + +export const sortieFactionToSystemIndexes: Record = { + FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18], + FC_CORPUS: [1, 4, 7, 8, 12, 15], + FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15], + FC_CORRUPTED: [14] +}; + +export const sortieFactionToFactionIndexes: Record = { + FC_GRINEER: [0], + FC_CORPUS: [1], + FC_INFESTATION: [0, 1, 2], + FC_CORRUPTED: [3] +}; + +export const sortieBossNode: Record = { + SORTIE_BOSS_HYENA: "SolNode127", + SORTIE_BOSS_KELA: "SolNode193", + SORTIE_BOSS_VOR: "SolNode108", + SORTIE_BOSS_RUK: "SolNode32", + SORTIE_BOSS_HEK: "SolNode24", + SORTIE_BOSS_KRIL: "SolNode99", + SORTIE_BOSS_TYL: "SolNode105", + SORTIE_BOSS_JACKAL: "SolNode104", + SORTIE_BOSS_ALAD: "SolNode53", + SORTIE_BOSS_AMBULAS: "SolNode51", + SORTIE_BOSS_NEF: "SettlementNode20", + SORTIE_BOSS_RAPTOR: "SolNode210", + SORTIE_BOSS_LEPHANTIS: "SolNode712", + SORTIE_BOSS_INFALAD: "SolNode705" +}; + +export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 + +export const getSortieTime = (day: number): number => { + const dayStart = EPOCH + day * 86400000; + const date = new Date(dayStart); + date.setUTCHours(12); + const isDst = new Intl.DateTimeFormat("en-US", { + timeZone: "America/Toronto", + timeZoneName: "short" + }) + .formatToParts(date) + .find(part => part.type === "timeZoneName")! + .value.includes("DT"); + return dayStart + (isDst ? 16 : 17) * 3600000; +}; diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 74463e4d..d9e228ae 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -62,23 +62,6 @@ } } ], - "Sorties": [ - { - "_id": { "$oid": "663a4c7d4d932c97c0a3acd7" }, - "Activation": { "$date": { "$numberLong": "1715097600000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Reward": "/Lotus/Types/Game/MissionDecks/SortieRewards", - "Seed": 24491, - "Boss": "SORTIE_BOSS_TYL", - "ExtraDrops": [], - "Variants": [ - { "missionType": "MT_TERRITORY", "modifierType": "SORTIE_MODIFIER_ARMOR", "node": "SolNode122", "tileset": "GrineerOceanTileset" }, - { "missionType": "MT_MOBILE_DEFENSE", "modifierType": "SORTIE_MODIFIER_LOW_ENERGY", "node": "SolNode184", "tileset": "GrineerGalleonTileset" }, - { "missionType": "MT_LANDSCAPE", "modifierType": "SORTIE_MODIFIER_EXIMUS", "node": "SolNode228", "tileset": "EidolonTileset" } - ], - "Twitter": true - } - ], "SyndicateMissions": [ { "_id": { "$oid": "663a4fc5ba6f84724fa48049" }, From 7f805a1dcc202a230543adc96f0609f60dfff70e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:29:32 -0700 Subject: [PATCH 401/776] feat: handle KeyToRemove in EOM upload (#1491) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1491 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 16 ++-- src/services/missionInventoryUpdateService.ts | 86 ++++++++++--------- src/types/requestTypes.ts | 3 + 3 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 904c90fa..92281629 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -538,15 +538,9 @@ export const addItem = async ( if (!key) return {}; return { QuestKeys: [key] }; } else { - const key = { ItemType: typeName, ItemCount: quantity }; - - const index = inventory.LevelKeys.findIndex(levelKey => levelKey.ItemType == typeName); - if (index != -1) { - inventory.LevelKeys[index].ItemCount += quantity; - } else { - inventory.LevelKeys.push(key); - } - return { LevelKeys: [key] }; + const levelKeyChanges = [{ ItemType: typeName, ItemCount: quantity }]; + addLevelKeys(inventory, levelKeyChanges); + return { LevelKeys: levelKeyChanges }; } } if (typeName in ExportDrones) { @@ -1240,6 +1234,10 @@ export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: IT applyArrayChanges(inventory.Recipes, itemsArray); }; +export const addLevelKeys = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.LevelKeys, itemsArray); +}; + export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => { const { RawUpgrades } = inventory; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 54620a1a..92f219e2 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -22,6 +22,7 @@ import { addFusionTreasures, addGearExpByCategory, addItem, + addLevelKeys, addMiscItems, addMissionComplete, addMods, @@ -77,19 +78,52 @@ export const addMissionInventoryUpdates = async ( inventoryUpdates: IMissionInventoryUpdateRequest ): Promise => { const inventoryChanges: IInventoryChanges = {}; - if ( - inventoryUpdates.EndOfMatchUpload && - inventoryUpdates.Missions && - inventoryUpdates.Missions.Tag in ExportRegions - ) { - const node = ExportRegions[inventoryUpdates.Missions.Tag]; - if (node.miscItemFee) { - addMiscItems(inventory, [ - { - ItemType: node.miscItemFee.ItemType, - ItemCount: node.miscItemFee.ItemCount * -1 - } - ]); + if (inventoryUpdates.EndOfMatchUpload) { + if (inventoryUpdates.Missions && inventoryUpdates.Missions.Tag in ExportRegions) { + const node = ExportRegions[inventoryUpdates.Missions.Tag]; + if (node.miscItemFee) { + addMiscItems(inventory, [ + { + ItemType: node.miscItemFee.ItemType, + ItemCount: node.miscItemFee.ItemCount * -1 + } + ]); + } + } + if (inventoryUpdates.KeyToRemove) { + if (!inventoryUpdates.KeyOwner || inventory.accountOwnerId.equals(inventoryUpdates.KeyOwner)) { + addLevelKeys(inventory, [ + { + ItemType: inventoryUpdates.KeyToRemove, + ItemCount: -1 + } + ]); + } + } + if ( + inventoryUpdates.MissionFailed && + inventoryUpdates.MissionStatus == "GS_FAILURE" && + inventoryUpdates.ObjectiveReached && + !inventoryUpdates.LockedWeaponGroup + ) { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; + const SuitId = new Types.ObjectId(config.s!.ItemId.$oid); + + inventory.BrandedSuits ??= []; + if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) { + inventory.BrandedSuits.push(SuitId); + + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + msg: "/Lotus/Language/G1Quests/BrandedMessage", + sub: "/Lotus/Language/G1Quests/BrandedTitle", + att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], + highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. + } + ]); + } } } if (inventoryUpdates.RewardInfo) { @@ -110,32 +144,6 @@ export const addMissionInventoryUpdates = async ( inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } } - if ( - inventoryUpdates.MissionFailed && - inventoryUpdates.MissionStatus == "GS_FAILURE" && - inventoryUpdates.EndOfMatchUpload && - inventoryUpdates.ObjectiveReached && - !inventoryUpdates.LockedWeaponGroup - ) { - const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; - const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; - const SuitId = new Types.ObjectId(config.s!.ItemId.$oid); - - inventory.BrandedSuits ??= []; - if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) { - inventory.BrandedSuits.push(SuitId); - - await createMessage(inventory.accountOwnerId, [ - { - sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", - msg: "/Lotus/Language/G1Quests/BrandedMessage", - sub: "/Lotus/Language/G1Quests/BrandedTitle", - att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], - highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. - } - ]); - } - } for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { if (value === undefined) { logger.error(`Inventory update key ${key} has no value `); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index d9139ec7..d3360e22 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -49,6 +49,9 @@ export type IMissionInventoryUpdateRequest = { rewardsMultiplier?: number; GoalTag: string; LevelKeyName: string; + KeyOwner?: string; + KeyRemovalHash?: string; + KeyToRemove?: string; ActiveBoosters?: IBooster[]; RawUpgrades?: IRawUpgrade[]; FusionTreasures?: IFusionTreasure[]; From 7fd4d50e07de1f1c8af22ea5564593a8db1fab1a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:29:44 -0700 Subject: [PATCH 402/776] feat(webui): add level keys via "add items" (#1493) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1493 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 7d60f896..9ef81613 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -218,6 +218,11 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(key.name || "", lang), chainLength: key.chainStages.length }); + } else if (key.name) { + res.miscitems.push({ + uniqueName, + name: getString(key.name, lang) + }); } } From 74c7d86090b0b15271bdebf965da44be2d7aa9d5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:30:00 -0700 Subject: [PATCH 403/776] feat: polychrome (#1495) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1495 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/placeDecoInComponentController.ts | 7 +++- .../api/setDojoComponentColorsController.ts | 34 +++++++++++++++++++ src/models/guildModel.ts | 8 ++++- src/routes/api.ts | 2 ++ src/services/guildService.ts | 29 ++++++++++++++++ src/types/guildTypes.ts | 13 ++++++- 6 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 src/controllers/api/setDojoComponentColorsController.ts diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index ea40dae3..7dab5bbf 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -64,7 +64,12 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = } if (!meta || (meta.price == 0 && meta.ingredients.length == 0)) { deco.CompletionTime = new Date(); - } else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { + } else if ( + guild.AutoContributeFromVault && + guild.VaultRegularCredits && + guild.VaultMiscItems && + deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco" + ) { if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { let enoughMiscItems = true; for (const ingredient of meta.ingredients) { diff --git a/src/controllers/api/setDojoComponentColorsController.ts b/src/controllers/api/setDojoComponentColorsController.ts new file mode 100644 index 00000000..ac069242 --- /dev/null +++ b/src/controllers/api/setDojoComponentColorsController.ts @@ -0,0 +1,34 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const setDojoComponentColorsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const data = getJSONfromString(String(req.body)); + const component = guild.DojoComponents.id(data.ComponentId)!; + //const deco = component.Decos!.find(x => x._id.equals(data.DecoId))!; + //deco.Pending = true; + //component.PaintBot = new Types.ObjectId(data.DecoId); + if ("lights" in req.query) { + component.PendingLights = data.Colours; + } else { + component.PendingColors = data.Colours; + } + await guild.save(); + res.json(await getDojoClient(guild, 0, component._id)); +}; + +interface ISetDojoComponentColorsRequest { + ComponentId: string; + DecoId: string; + Colours: number[]; +} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 563cb7d2..df2f860a 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -29,7 +29,8 @@ const dojoDecoSchema = new Schema({ MiscItems: { type: [typeCountSchema], default: undefined }, CompletionTime: Date, RushPlatinum: Number, - PictureFrameInfo: pictureFrameInfoSchema + PictureFrameInfo: pictureFrameInfoSchema, + Pending: Boolean }); const dojoLeaderboardEntrySchema = new Schema( @@ -57,6 +58,11 @@ const dojoComponentSchema = new Schema({ DestructionTime: Date, Decos: [dojoDecoSchema], DecoCapacity: Number, + PaintBot: Schema.Types.ObjectId, + PendingColors: { type: [Number], default: undefined }, + Colors: { type: [Number], default: undefined }, + PendingLights: { type: [Number], default: undefined }, + Lights: { type: [Number], default: undefined }, Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined } }); diff --git a/src/routes/api.ts b/src/routes/api.ts index 81415dc6..7970c29d 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -105,6 +105,7 @@ import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestCo import { setActiveShipController } from "@/src/controllers/api/setActiveShipController"; import { setAllianceGuildPermissionsController } from "@/src/controllers/api/setAllianceGuildPermissionsController"; import { setBootLocationController } from "@/src/controllers/api/setBootLocationController"; +import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoComponentColorsController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; @@ -261,6 +262,7 @@ apiRouter.post("/saveLoadout.php", saveLoadoutController); apiRouter.post("/saveSettings.php", saveSettingsController); apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController); apiRouter.post("/sell.php", sellController); +apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); apiRouter.post("/setGuildMotd.php", setGuildMotdController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 6388acef..32ff25a9 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -141,6 +141,7 @@ export const getDojoClient = async ( DojoComponents: [] }; const roomsToRemove: Types.ObjectId[] = []; + const decosToRemoveNoRefund: { componentId: Types.ObjectId; decoId: Types.ObjectId }[] = []; let needSave = false; for (const dojoComponent of guild.DojoComponents) { if (!componentId || dojoComponent._id.equals(componentId)) { @@ -212,6 +213,21 @@ export const getDojoClient = async ( PictureFrameInfo: deco.PictureFrameInfo }; if (deco.CompletionTime) { + if ( + deco.Type == "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco" && + Date.now() >= deco.CompletionTime.getTime() + ) { + if (dojoComponent.PendingColors) { + dojoComponent.Colors = dojoComponent.PendingColors; + dojoComponent.PendingColors = undefined; + } + if (dojoComponent.PendingLights) { + dojoComponent.Lights = dojoComponent.PendingLights; + dojoComponent.PendingLights = undefined; + } + decosToRemoveNoRefund.push({ componentId: dojoComponent._id, decoId: deco._id }); + continue; + } clientDeco.CompletionTime = toMongoDate(deco.CompletionTime); } else { clientDeco.RegularCredits = deco.RegularCredits; @@ -220,6 +236,10 @@ export const getDojoClient = async ( clientComponent.Decos.push(clientDeco); } } + clientComponent.PendingColors = dojoComponent.PendingColors; + clientComponent.Colors = dojoComponent.Colors; + clientComponent.PendingLights = dojoComponent.PendingLights; + clientComponent.Lights = dojoComponent.Lights; dojo.DojoComponents.push(clientComponent); } } @@ -230,6 +250,15 @@ export const getDojoClient = async ( } needSave = true; } + for (const deco of decosToRemoveNoRefund) { + logger.debug(`removing polychrome`, deco); + const component = guild.DojoComponents.id(deco.componentId)!; + component.Decos!.splice( + component.Decos!.findIndex(x => x._id.equals(deco.decoId)), + 1 + ); + needSave = true; + } if (needSave) { await guild.save(); } diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 8af68809..7746f2bb 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -161,6 +161,7 @@ export interface IDojoClient { export interface IDojoComponentClient { id: IOid; + SortId?: IOid; pf: string; // Prefab (.level) ppf: string; pi?: IOid; // Parent ID. N/A to root. @@ -175,16 +176,25 @@ export interface IDojoComponentClient { DestructionTime?: IMongoDate; Decos?: IDojoDecoClient[]; DecoCapacity?: number; + PaintBot?: IOid; + PendingColors?: number[]; + Colors?: number[]; + PendingLights?: number[]; + Lights?: number[]; } export interface IDojoComponentDatabase - extends Omit { + extends Omit< + IDojoComponentClient, + "id" | "SortId" | "pi" | "CompletionTime" | "DestructionTime" | "Decos" | "PaintBot" + > { _id: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; CompletionLogPending?: boolean; DestructionTime?: Date; Decos?: IDojoDecoDatabase[]; + PaintBot?: Types.ObjectId; Leaderboard?: IDojoLeaderboardEntry[]; } @@ -200,6 +210,7 @@ export interface IDojoDecoClient { CompletionTime?: IMongoDate; RushPlatinum?: number; PictureFrameInfo?: IPictureFrameInfo; + Pending?: boolean; } export interface IDojoDecoDatabase extends Omit { From dd32e082f333e16142446109bef97865007b342b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:30:15 -0700 Subject: [PATCH 404/776] chore: add UmbraDate to equipment (#1496) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1496 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 4 ++++ src/services/importService.ts | 1 + src/types/inventoryTypes/commonInventoryTypes.ts | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a142273e..1b990228 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -872,6 +872,7 @@ const EquipmentSchema = new Schema( OffensiveUpgrade: String, DefensiveUpgrade: String, UpgradesExpiry: Date, + UmbraDate: Date, ArchonCrystalUpgrades: { type: [ArchonCrystalUpgradeSchema], default: undefined }, Weapon: crewShipWeaponSchema, Customization: crewShipCustomizationSchema, @@ -902,6 +903,9 @@ EquipmentSchema.set("toJSON", { if (db.UpgradesExpiry) { client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry); } + if (db.UmbraDate) { + client.UmbraDate = toMongoDate(db.UmbraDate); + } } }); diff --git a/src/services/importService.ts b/src/services/importService.ts index ae16e86d..e4f6b97c 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -54,6 +54,7 @@ const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => { InfestationDate: convertOptionalDate(client.InfestationDate), Expiry: convertOptionalDate(client.Expiry), UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry), + UmbraDate: convertOptionalDate(client.UmbraDate), CrewMembers: client.CrewMembers ? convertCrewShipMembers(client.CrewMembers) : undefined, Details: client.Details ? convertKubrowDetails(client.Details) : undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/src/types/inventoryTypes/commonInventoryTypes.ts b/src/types/inventoryTypes/commonInventoryTypes.ts index ea26992a..06a8ec9c 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -90,12 +90,13 @@ export interface IEquipmentSelection { export interface IEquipmentClient extends Omit< IEquipmentDatabase, - "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "CrewMembers" | "Details" + "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details" > { ItemId: IOid; InfestationDate?: IMongoDate; Expiry?: IMongoDate; UpgradesExpiry?: IMongoDate; + UmbraDate?: IMongoDate; CrewMembers?: ICrewShipMembersClient; Details?: IKubrowPetDetailsClient; } @@ -134,6 +135,7 @@ export interface IEquipmentDatabase { OffensiveUpgrade?: string; DefensiveUpgrade?: string; UpgradesExpiry?: Date; + UmbraDate?: Date; // related to scrapped "echoes of umbra" feature ArchonCrystalUpgrades?: IArchonCrystalUpgrade[]; Weapon?: ICrewShipWeapon; Customization?: ICrewShipCustomization; From a2f1469779d838b82e3d553a86f13cadc6757532 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:30:29 -0700 Subject: [PATCH 405/776] feat: add attVisualOnly to inbox messages (#1499) In case we'll need it... Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1499 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 | 4 ++-- src/models/inboxModel.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 7cb777e2..4c3b8830 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -34,8 +34,8 @@ export const inboxController: RequestHandler = async (req, res) => { message.r = true; await message.save(); - const attachmentItems = message.att; - const attachmentCountedItems = message.countedAtt; + const attachmentItems = message.attVisualOnly ? undefined : message.att; + const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt; if (!attachmentItems && !attachmentCountedItems && !message.gifts) { res.status(200).end(); diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index c2d8af44..85dff3fb 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -4,7 +4,8 @@ import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; -export interface IMessageClient extends Omit { +export interface IMessageClient + extends Omit { _id?: IOid; date: IMongoDate; startDate?: IMongoDate; @@ -29,6 +30,7 @@ export interface IMessage { endDate?: Date; att?: string[]; countedAtt?: ITypeCount[]; + attVisualOnly?: boolean; transmission?: string; arg?: Arg[]; gifts?: IGift[]; @@ -108,6 +110,7 @@ const messageSchema = new Schema( att: { type: [String], default: undefined }, gifts: { type: [giftSchema], default: undefined }, countedAtt: { type: [typeCountSchema], default: undefined }, + attVisualOnly: Boolean, transmission: String, arg: { type: [ @@ -141,6 +144,7 @@ messageSchema.set("toJSON", { delete returnedObject._id; delete returnedObject.__v; + delete returnedObject.attVisualOnly; messageClient.date = toMongoDate(messageDatabase.date); From 8ce86ad4fd7b9c9dcb7d86fae5d4379e8b6b13a8 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 7 Apr 2025 05:55:33 -0700 Subject: [PATCH 406/776] chore(webui): show quantity for recipes (#1506) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1506 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 9ef81613..63bf949f 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -150,9 +150,11 @@ const getItemListsController: RequestHandler = (req, response) => { if (!item.hidden) { const resultName = getItemName(item.resultType); if (resultName) { + let itemName = getString(resultName, lang); + if (item.num > 1) itemName = `${itemName} X ${item.num}`; res.miscitems.push({ uniqueName: uniqueName, - name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang)) + name: recipeNameTemplate.replace("|ITEM|", itemName) }); } } From dcdeb0cd341ae03b26f6a79762a7a7fd834edae2 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Tue, 8 Apr 2025 03:05:53 -0700 Subject: [PATCH 407/776] feat(webui): add pigment (#1507) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1507 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/script.js | 5 +++++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 6 files changed, 10 insertions(+) diff --git a/static/webui/script.js b/static/webui/script.js index 849062b2..1dd0bb22 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -221,6 +221,11 @@ function fetchItemList() { name: loc("code_zanuka") }); + data.miscitems.push({ + uniqueName: "/Lotus/Types/Items/Research/DojoColors/GenericDojoColorPigment", + name: loc("code_pigment") + }); + const itemMap = { // Generics for rivens "/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": { name: loc("code_archgun") }, diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 1fffac3a..51250542 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -53,6 +53,7 @@ dict = { code_setInactive: `Quest inaktiv setzen`, code_completed: `Abgeschlossen`, code_active: `Aktiv`, + code_pigment: `Pigment`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 798c69db..88466019 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -52,6 +52,7 @@ dict = { code_setInactive: `Make the quest inactive`, code_completed: `Completed`, code_active: `Active`, + code_pigment: `Pigment`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index eb635c5a..2e119445 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -53,6 +53,7 @@ dict = { code_setInactive: `[UNTRANSLATED] Make the quest inactive`, code_completed: `[UNTRANSLATED] Completed`, code_active: `[UNTRANSLATED] Active`, + code_pigment: `Pigment`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 893a864d..1543b848 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -53,6 +53,7 @@ dict = { code_setInactive: `Сделать квест неактивным`, code_completed: `Завершено`, code_active: `Активный`, + code_pigment: `Пигмент`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 29890fd0..423a47df 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -53,6 +53,7 @@ dict = { code_setInactive: `[UNTRANSLATED] Make the quest inactive`, code_completed: `[UNTRANSLATED] Completed`, code_active: `[UNTRANSLATED] Active`, + code_pigment: `颜料`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, From 7f69667171e44319124ae03b5bb037e02c1606aa Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 8 Apr 2025 03:06:06 -0700 Subject: [PATCH 408/776] feat: dojo component settings (#1509) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1509 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/setDojoComponentSettingsController.ts | 25 +++++++++++++++++++ src/models/guildModel.ts | 1 + src/routes/api.ts | 2 ++ src/services/guildService.ts | 3 ++- src/types/guildTypes.ts | 1 + 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/setDojoComponentSettingsController.ts diff --git a/src/controllers/api/setDojoComponentSettingsController.ts b/src/controllers/api/setDojoComponentSettingsController.ts new file mode 100644 index 00000000..e7118415 --- /dev/null +++ b/src/controllers/api/setDojoComponentSettingsController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const setDojoComponentSettingsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const component = guild.DojoComponents.id(req.query.componentId)!; + const data = getJSONfromString(String(req.body)); + component.Settings = data.Settings; + await guild.save(); + res.json(await getDojoClient(guild, 0, component._id)); +}; + +interface ISetDojoComponentSettingsRequest { + Settings: string; +} diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index df2f860a..2d757039 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -63,6 +63,7 @@ const dojoComponentSchema = new Schema({ Colors: { type: [Number], default: undefined }, PendingLights: { type: [Number], default: undefined }, Lights: { type: [Number], default: undefined }, + Settings: String, Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined } }); diff --git a/src/routes/api.ts b/src/routes/api.ts index 7970c29d..d8be0cb3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -107,6 +107,7 @@ import { setAllianceGuildPermissionsController } from "@/src/controllers/api/set import { setBootLocationController } from "@/src/controllers/api/setBootLocationController"; import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoComponentColorsController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; +import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; @@ -264,6 +265,7 @@ apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController apiRouter.post("/sell.php", sellController); apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); +apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); apiRouter.post("/setGuildMotd.php", setGuildMotdController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 32ff25a9..f8e33a5e 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -151,7 +151,8 @@ export const getDojoClient = async ( ppf: dojoComponent.ppf, Name: dojoComponent.Name, Message: dojoComponent.Message, - DecoCapacity: dojoComponent.DecoCapacity ?? 600 + DecoCapacity: dojoComponent.DecoCapacity ?? 600, + Settings: dojoComponent.Settings }; if (dojoComponent.pi) { clientComponent.pi = toOid(dojoComponent.pi); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 7746f2bb..2502b57e 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -181,6 +181,7 @@ export interface IDojoComponentClient { Colors?: number[]; PendingLights?: number[]; Lights?: number[]; + Settings?: string; } export interface IDojoComponentDatabase From ef4973e69436dd1dfee2f75c52b36e88318beac4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 8 Apr 2025 03:06:19 -0700 Subject: [PATCH 409/776] chrore(webui): don't add duplicates to datalists (#1510) Browsers will show all options, even if this makes no sense, causing some confusion for users. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1510 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index 1dd0bb22..d55219a0 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -265,6 +265,7 @@ function fetchItemList() { } else if (type == "uniqueLevelCaps") { uniqueLevelCaps = items; } else { + const nameSet = new Set(); items.forEach(item => { if (item.name.includes(" ")) { item.name = item.name.replace(" ", ""); @@ -311,20 +312,19 @@ function fetchItemList() { document .getElementById("datalist-" + type + "-" + item.partType.slice(5)) .appendChild(option); + } + } else if (item.badReason != "notraw") { + if (nameSet.has(item.name)) { + //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`); } else { - console.log(item.partType); + nameSet.add(item.name); + const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; document.getElementById("datalist-" + type).appendChild(option); } } - if (item.badReason != "notraw") { - const option = document.createElement("option"); - option.setAttribute("data-key", item.uniqueName); - option.value = item.name; - document.getElementById("datalist-" + type).appendChild(option); - } itemMap[item.uniqueName] = { ...item, type }; }); } From 39be09581883d0ff795307e9a38c1f0f64858552 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 8 Apr 2025 03:06:36 -0700 Subject: [PATCH 410/776] chore: handle season challenge completion in missionInventoryUpdate (#1511) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1511 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/updateChallengeProgressController.ts | 44 ++++---------- src/services/inventoryService.ts | 58 ++++++++++++------- src/services/missionInventoryUpdateService.ts | 2 +- 3 files changed, 49 insertions(+), 55 deletions(-) diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index 3e056538..93135634 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -1,10 +1,8 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService"; +import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; -import { ExportNightwave } from "warframe-public-export-plus"; -import { logger } from "@/src/utils/logger"; import { IAffiliationMods } from "@/src/types/purchaseTypes"; export const updateChallengeProgressController: RequestHandler = async (req, res) => { @@ -12,41 +10,19 @@ export const updateChallengeProgressController: RequestHandler = async (req, res const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations"); + let affiliationMods: IAffiliationMods[] = []; if (challenges.ChallengeProgress) { - addChallenges(inventory, challenges.ChallengeProgress); + affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions); } if (challenges.SeasonChallengeHistory) { - addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); - } - const affiliationMods: IAffiliationMods[] = []; - if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) { - for (const challenge of challenges.SeasonChallengeCompletions) { - // Ignore challenges that weren't completed just now - if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) { - continue; + challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { + const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge); + if (itemIndex !== -1) { + inventory.SeasonChallengeHistory[itemIndex].id = id; + } else { + inventory.SeasonChallengeHistory.push({ challenge, id }); } - - const meta = ExportNightwave.challenges[challenge.challenge]; - logger.debug("Completed challenge", meta); - - let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag); - if (!affiliation) { - affiliation = - inventory.Affiliations[ - inventory.Affiliations.push({ - Tag: ExportNightwave.affiliationTag, - Standing: 0 - }) - 1 - ]; - } - affiliation.Standing += meta.standing; - - if (affiliationMods.length == 0) { - affiliationMods.push({ Tag: ExportNightwave.affiliationTag }); - } - affiliationMods[0].Standing ??= 0; - affiliationMods[0].Standing += meta.standing; - } + }); } await inventory.save(); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 92281629..67ca9f9f 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1,7 +1,7 @@ import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; import { Types } from "mongoose"; -import { SlotNames, IInventoryChanges, IBinChanges, slotNames } from "@/src/types/purchaseTypes"; +import { SlotNames, IInventoryChanges, IBinChanges, slotNames, IAffiliationMods } from "@/src/types/purchaseTypes"; import { IChallengeProgress, IFlavourItem, @@ -45,6 +45,7 @@ import { ExportGear, ExportKeys, ExportMisc, + ExportNightwave, ExportRailjackWeapons, ExportRecipes, ExportResources, @@ -1302,35 +1303,52 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); }; -export const addSeasonalChallengeHistory = ( +export const addChallenges = ( inventory: TInventoryDatabaseDocument, - itemsArray: ISeasonChallenge[] -): void => { - const category = inventory.SeasonChallengeHistory; - - itemsArray.forEach(({ challenge, id }) => { - const itemIndex = category.findIndex(i => i.challenge === challenge); + ChallengeProgress: IChallengeProgress[], + SeasonChallengeCompletions: ISeasonChallenge[] | undefined +): IAffiliationMods[] => { + ChallengeProgress.forEach(({ Name, Progress }) => { + const itemIndex = inventory.ChallengeProgress.findIndex(i => i.Name === Name); if (itemIndex !== -1) { - category[itemIndex].id = id; + inventory.ChallengeProgress[itemIndex].Progress = Progress; } else { - category.push({ challenge, id }); + inventory.ChallengeProgress.push({ Name, Progress }); } }); -}; -export const addChallenges = (inventory: TInventoryDatabaseDocument, itemsArray: IChallengeProgress[]): void => { - const category = inventory.ChallengeProgress; + const affiliationMods: IAffiliationMods[] = []; + if (SeasonChallengeCompletions) { + for (const challenge of SeasonChallengeCompletions) { + // Ignore challenges that weren't completed just now + if (!ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) { + continue; + } - itemsArray.forEach(({ Name, Progress }) => { - const itemIndex = category.findIndex(i => i.Name === Name); + const meta = ExportNightwave.challenges[challenge.challenge]; + logger.debug("Completed challenge", meta); - if (itemIndex !== -1) { - category[itemIndex].Progress = Progress; - } else { - category.push({ Name, Progress }); + let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag); + if (!affiliation) { + affiliation = + inventory.Affiliations[ + inventory.Affiliations.push({ + Tag: ExportNightwave.affiliationTag, + Standing: 0 + }) - 1 + ]; + } + affiliation.Standing += meta.standing; + + if (affiliationMods.length == 0) { + affiliationMods.push({ Tag: ExportNightwave.affiliationTag }); + } + affiliationMods[0].Standing ??= 0; + affiliationMods[0].Standing += meta.standing; } - }); + } + return affiliationMods; }; export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 92f219e2..b23adb3c 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -195,7 +195,7 @@ export const addMissionInventoryUpdates = async ( addRecipes(inventory, value); break; case "ChallengeProgress": - addChallenges(inventory, value); + addChallenges(inventory, value, inventoryUpdates.SeasonChallengeCompletions); break; case "FusionTreasures": addFusionTreasures(inventory, value); From 327b834b074efde5ebbab47d6ce796236985205d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 8 Apr 2025 03:06:47 -0700 Subject: [PATCH 411/776] chore: handle zealoid prelate stripped rewards (#1515) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1515 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 22 +++++++++++++++---- src/types/requestTypes.ts | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b23adb3c..ab5bdcdb 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -602,10 +602,24 @@ export const addMissionRewards = async ( if (!droptable) { logger.error(`unknown droptable ${si.DropTable}`); } else { - for (let i = 0; i != si.DROP_MOD.length; ++i) { - for (const pool of droptable) { - const reward = getRandomReward(pool.items)!; - logger.debug(`stripped droptable rolled`, reward); + const modsPool = droptable[0].items; + const blueprintsPool = (droptable.length > 1 ? droptable[1] : droptable[0]).items; + if (si.DROP_MOD) { + for (let i = 0; i != si.DROP_MOD.length; ++i) { + const reward = getRandomReward(modsPool)!; + logger.debug(`stripped droptable (mods pool) rolled`, reward); + await addItem(inventory, reward.type); + MissionRewards.push({ + StoreItem: toStoreItem(reward.type), + ItemCount: 1, + FromEnemyCache: true // to show "identified" + }); + } + } + if (si.DROP_BLUEPRINT) { + for (let i = 0; i != si.DROP_BLUEPRINT.length; ++i) { + const reward = getRandomReward(blueprintsPool)!; + logger.debug(`stripped droptable (blueprints pool) rolled`, reward); await addItem(inventory, reward.type); MissionRewards.push({ StoreItem: toStoreItem(reward.type), diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index d3360e22..173d150d 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -101,7 +101,8 @@ export type IMissionInventoryUpdateRequest = { Upgrades?: IUpgradeClient[]; // riven challenge progress StrippedItems?: { DropTable: string; - DROP_MOD: number[]; + DROP_MOD?: number[]; + DROP_BLUEPRINT?: number[]; }[]; DeathMarks?: string[]; Nemesis?: number; From bb315eaafefb888f854d3a1138733fd5f96c4f4b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 03:48:07 -0700 Subject: [PATCH 412/776] chore: handle addItem of crew ship harness (#1516) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1516 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 67ca9f9f..08ed667e 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -659,6 +659,8 @@ export const addItem = async ( return { MiscItems: miscItemChanges }; + } else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") { + return addCrewShipHarness(inventory, typeName); } break; } From 5cd18db7a747afb67aedc616c95afad5f061342e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:30:04 +0200 Subject: [PATCH 413/776] chore: prettier --- .vscode/extensions.json | 6 ++---- static/webui/translations/de.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 897af65d..940260d8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,3 @@ { - "recommendations": [ - "dbaeumer.vscode-eslint" - ] -} \ No newline at end of file + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 51250542..7dba3ab2 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -133,7 +133,7 @@ dict = { cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, - cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, + cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, From 005350bde0623c5de1a22a0823914776595f172b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 06:52:30 -0700 Subject: [PATCH 414/776] ci: remove node.js version matrix (#1519) We only check if the TypeScript successfully compiles & lints, which isn't really dependent on Node.js version. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1519 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .github/workflows/build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5decfe6d..211a31c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,16 +5,11 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - version: [18, 20, 22] steps: - name: Checkout uses: actions/checkout@v4.1.2 - name: Setup Node.js environment uses: actions/setup-node@v4.0.2 - with: - node-version: ${{ matrix.version }} - run: npm ci - run: cp config.json.example config.json - run: npm run verify From db1dd21924d146a14370d6f2055ea5d681251222 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:25:19 -0700 Subject: [PATCH 415/776] ci: improve prettier coverage (#1523) All prettier violations will now be reported, not just what eslint checks. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1523 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .github/workflows/build.yml | 11 ++++++++++- package.json | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 211a31c9..24a2f307 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,4 +13,13 @@ jobs: - run: npm ci - run: cp config.json.example config.json - run: npm run verify - - run: npm run lint + - run: npm run lint:ci + - run: npm run prettier + - name: Fail if there are uncommitted changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "Uncommitted changes detected:" + git status + git diff + exit 1 + fi diff --git a/package.json b/package.json index 3c0c536d..899947c7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "verify": "tsgo --noEmit", "lint": "eslint --ext .ts .", + "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .", "lint:fix": "eslint --fix --ext .ts .", "prettier": "prettier --write .", "update-translations": "cd scripts && node update-translations.js" From a8f174bce19dc6f2ccb44e1ea43998b71e34192e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:26:40 -0700 Subject: [PATCH 416/776] fix: don't duplicate FlavourItems (#1526) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1526 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 08ed667e..078e7cd2 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1014,12 +1014,14 @@ export const addCustomization = ( customizationName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - inventoryChanges.FlavourItems ??= []; - (inventoryChanges.FlavourItems as IFlavourItem[]).push( - inventory.FlavourItems[flavourItemIndex].toJSON() - ); + if (!inventory.FlavourItems.find(x => x.ItemType == customizationName)) { + const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.FlavourItems ??= []; + (inventoryChanges.FlavourItems as IFlavourItem[]).push( + inventory.FlavourItems[flavourItemIndex].toJSON() + ); + } return inventoryChanges; }; From 02a4d2b30ab8b4b89f086d9e10abca5e7f0d2665 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:28:29 -0700 Subject: [PATCH 417/776] feat: track KIM resets (#1528) This was added in 38.5.0 for FlareRank1Convo3 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1528 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/clearDialogueHistoryController.ts | 2 ++ src/models/inventoryModels/inventoryModel.ts | 1 + src/types/inventoryTypes/inventoryTypes.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/controllers/api/clearDialogueHistoryController.ts b/src/controllers/api/clearDialogueHistoryController.ts index 96c8e1c6..f24f360a 100644 --- a/src/controllers/api/clearDialogueHistoryController.ts +++ b/src/controllers/api/clearDialogueHistoryController.ts @@ -7,6 +7,8 @@ export const clearDialogueHistoryController: RequestHandler = async (req, res) = const inventory = await getInventory(accountId); const request = JSON.parse(String(req.body)) as IClearDialogueRequest; if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) { + inventory.DialogueHistory.Resets ??= 0; + inventory.DialogueHistory.Resets += 1; for (const dialogueName of request.Dialogues) { const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName); if (index != -1) { diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 1b990228..fb6e82bd 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -797,6 +797,7 @@ dialogueSchema.set("toJSON", { const dialogueHistorySchema = new Schema( { YearIteration: { type: Number, required: true }, + Resets: Number, Dialogues: { type: [dialogueSchema], required: false } }, { _id: false } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index c25b7123..7c3431a1 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1074,11 +1074,13 @@ export interface IEndlessXpProgress { export interface IDialogueHistoryClient { YearIteration: number; + Resets?: number; // added in 38.5.0 Dialogues?: IDialogueClient[]; } export interface IDialogueHistoryDatabase { YearIteration: number; + Resets?: number; Dialogues?: IDialogueDatabase[]; } From 74f9d1567f130f23b0dcdd6a63081f64569bdb39 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:30:59 -0700 Subject: [PATCH 418/776] feat: handle QueuedDialogues in saveDialogue (#1524) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1524 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 7 ++++--- src/models/inventoryModels/inventoryModel.ts | 2 +- src/types/inventoryTypes/inventoryTypes.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 55819636..a2328488 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -21,7 +21,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => { if (!inventory.DialogueHistory) { throw new Error("bad inventory state"); } - if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) { + if (request.OtherDialogueInfos.length != 0) { logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); } inventory.DialogueHistory.Dialogues ??= []; @@ -36,6 +36,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => { AvailableGiftDate: new Date(0), RankUpExpiry: new Date(0), BountyChemExpiry: new Date(0), + QueuedDialogues: [], Gifts: [], Booleans: [], Completed: [], @@ -45,7 +46,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => { } dialogue.Rank = request.Rank; dialogue.Chemistry = request.Chemistry; - //dialogue.QueuedDialogues = request.QueuedDialogues; + dialogue.QueuedDialogues = request.QueuedDialogues; for (const bool of request.Booleans) { dialogue.Booleans.push(bool); } @@ -77,7 +78,7 @@ interface SaveCompletedDialogueRequest { Rank: number; Chemistry: number; CompletionType: number; - QueuedDialogues: string[]; // unsure + QueuedDialogues: string[]; Booleans: string[]; ResetBooleans: string[]; Data: ICompletedDialogue; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index fb6e82bd..b4285d5f 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -773,7 +773,7 @@ const dialogueSchema = new Schema( AvailableGiftDate: Date, RankUpExpiry: Date, BountyChemExpiry: Date, - //QueuedDialogues: ??? + QueuedDialogues: { type: [String], default: [] }, Gifts: { type: [dialogueGiftSchema], default: [] }, Booleans: { type: [String], default: [] }, Completed: { type: [completedDialogueSchema], default: [] }, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 7c3431a1..fd957a82 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1091,7 +1091,7 @@ export interface IDialogueClient { AvailableGiftDate: IMongoDate; RankUpExpiry: IMongoDate; BountyChemExpiry: IMongoDate; - //QueuedDialogues: any[]; + QueuedDialogues: string[]; Gifts: IDialogueGift[]; Booleans: string[]; Completed: ICompletedDialogue[]; From 541b8d32a8b10b3b242e5e3c18faa6a61d0c892e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:37:28 -0700 Subject: [PATCH 419/776] feat: LizzieShawzin (#1525) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1525 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index a2328488..ccf67fc7 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,6 +1,7 @@ -import { getInventory } from "@/src/services/inventoryService"; +import { addEmailItem, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; @@ -24,6 +25,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => { if (request.OtherDialogueInfos.length != 0) { logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); } + const inventoryChanges: IInventoryChanges = {}; inventory.DialogueHistory.Dialogues ??= []; let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName); if (!dialogue) { @@ -49,6 +51,13 @@ export const saveDialogueController: RequestHandler = async (req, res) => { dialogue.QueuedDialogues = request.QueuedDialogues; for (const bool of request.Booleans) { dialogue.Booleans.push(bool); + if (bool == "LizzieShawzin") { + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem", + inventoryChanges + ); + } } for (const bool of request.ResetBooleans) { const index = dialogue.Booleans.findIndex(x => x == bool); @@ -61,7 +70,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => { dialogue.AvailableDate = new Date(tomorrowAt0Utc); await inventory.save(); res.json({ - InventoryChanges: [], + InventoryChanges: inventoryChanges, AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } }); } From 5692a6201e30eb381258cbbfa9e1c76603f7416d Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:14:33 -0700 Subject: [PATCH 420/776] feat: No Dojo Deco Build Stage cheat (#1508) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1508 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- config.json.example | 1 + .../api/placeDecoInComponentController.ts | 69 ++++++++++--------- src/services/configService.ts | 1 + static/webui/index.html | 4 ++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 9 files changed, 46 insertions(+), 34 deletions(-) diff --git a/config.json.example b/config.json.example index 4e52c750..626d2d7b 100644 --- a/config.json.example +++ b/config.json.example @@ -34,6 +34,7 @@ "noVendorPurchaseLimits": true, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": false, + "noDecoBuildStage": false, "fastDojoRoomDestruction": false, "noDojoResearchCosts": false, "noDojoResearchTime": false, diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index 7dab5bbf..cf50a90b 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -13,6 +13,7 @@ import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; import { Types } from "mongoose"; import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; export const placeDecoInComponentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -62,42 +63,42 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1; } } - if (!meta || (meta.price == 0 && meta.ingredients.length == 0)) { - deco.CompletionTime = new Date(); - } else if ( - guild.AutoContributeFromVault && - guild.VaultRegularCredits && - guild.VaultMiscItems && - deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco" - ) { - if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { - let enoughMiscItems = true; - for (const ingredient of meta.ingredients) { - if ( - getVaultMiscItemCount(guild, ingredient.ItemType) < - scaleRequiredCount(guild.Tier, ingredient.ItemCount) - ) { - enoughMiscItems = false; - break; - } - } - if (enoughMiscItems) { - guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price); - deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); - - deco.MiscItems = []; - for (const ingredient of meta.ingredients) { - guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -= - scaleRequiredCount(guild.Tier, ingredient.ItemCount); - deco.MiscItems.push({ - ItemType: ingredient.ItemType, - ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount) - }); - } - - deco.CompletionTime = new Date(Date.now() + meta.time * 1000); + if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") { + if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) { + deco.CompletionTime = new Date(); + if (meta) { processDojoBuildMaterialsGathered(guild, meta); } + } else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { + if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { + let enoughMiscItems = true; + for (const ingredient of meta.ingredients) { + if ( + getVaultMiscItemCount(guild, ingredient.ItemType) < + scaleRequiredCount(guild.Tier, ingredient.ItemCount) + ) { + enoughMiscItems = false; + break; + } + } + if (enoughMiscItems) { + guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price); + deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); + + deco.MiscItems = []; + for (const ingredient of meta.ingredients) { + guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -= + scaleRequiredCount(guild.Tier, ingredient.ItemCount); + deco.MiscItems.push({ + ItemType: ingredient.ItemType, + ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount) + }); + } + + deco.CompletionTime = new Date(Date.now() + meta.time * 1000); + processDojoBuildMaterialsGathered(guild, meta); + } + } } } } diff --git a/src/services/configService.ts b/src/services/configService.ts index 04c47d36..86461644 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -39,6 +39,7 @@ interface IConfig { noVendorPurchaseLimits?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; + noDojoDecoBuildStage?: boolean; fastDojoRoomDestruction?: boolean; noDojoResearchCosts?: boolean; noDojoResearchTime?: boolean; diff --git a/static/webui/index.html b/static/webui/index.html index 6468d53a..b567687e 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -608,6 +608,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 7dba3ab2..6d20ca01 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -137,6 +137,7 @@ dict = { cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, + cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, cheats_noDojoResearchCosts: `Keine Dojo-Forschungskosten`, cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 88466019..12638068 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -136,6 +136,7 @@ dict = { cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `No Dojo Research Costs`, cheats_noDojoResearchTime: `No Dojo Research Time`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 2e119445..6d619821 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -137,6 +137,7 @@ dict = { cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, cheats_noDojoResearchCosts: `Aucun coût de recherche (Dojo)`, cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 1543b848..62ff74c7 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -137,6 +137,7 @@ dict = { cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, + cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 423a47df..246d6dca 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -137,6 +137,7 @@ dict = { cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, + cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `快速拆除道场房间`, cheats_noDojoResearchCosts: `无视道场研究消耗`, cheats_noDojoResearchTime: `无视道场研究时间`, From 540961ff9e425da007175d1926b8401e3d92e1ab Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:14:53 -0700 Subject: [PATCH 421/776] chore(webui): use gildWeaponController (#1518) also use `TEquipmentKey` instead `WeaponTypeInternal | "Hoverboards"` Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/gildWeaponController.ts | 54 +++++++++---------- .../custom/gildEquipmentController.ts | 23 -------- src/routes/custom.ts | 2 - static/webui/script.js | 7 ++- 4 files changed, 27 insertions(+), 59 deletions(-) delete mode 100644 src/controllers/custom/gildEquipmentController.ts diff --git a/src/controllers/api/gildWeaponController.ts b/src/controllers/api/gildWeaponController.ts index 3914b81a..fac741fc 100644 --- a/src/controllers/api/gildWeaponController.ts +++ b/src/controllers/api/gildWeaponController.ts @@ -2,36 +2,25 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; -import { WeaponTypeInternal } from "@/src/services/itemDataService"; +import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ExportRecipes } from "warframe-public-export-plus"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [ - "LongGuns", - "Pistols", - "Melee", - "OperatorAmps", - "Hoverboards" -]; - interface IGildWeaponRequest { ItemName: string; Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint PolarizeSlot?: number; PolarizeValue?: ArtifactPolarity; ItemId: string; - Category: WeaponTypeInternal | "Hoverboards"; + Category: TEquipmentKey; } export const gildWeaponController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const data = getJSONfromString(String(req.body)); data.ItemId = String(req.query.ItemId); - if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) { - throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`); - } - data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards"; + data.Category = req.query.Category as TEquipmentKey; const inventory = await getInventory(accountId); const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId); @@ -42,8 +31,10 @@ export const gildWeaponController: RequestHandler = async (req, res) => { const weapon = inventory[data.Category][weaponIndex]; weapon.Features ??= 0; weapon.Features |= EquipmentFeatures.GILDED; - weapon.ItemName = data.ItemName; - weapon.XP = 0; + if (data.Recipe != "webui") { + weapon.ItemName = data.ItemName; + weapon.XP = 0; + } if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { weapon.Polarity = [ { @@ -56,21 +47,24 @@ export const gildWeaponController: RequestHandler = async (req, res) => { const inventoryChanges: IInventoryChanges = {}; inventoryChanges[data.Category] = [weapon.toJSON()]; - const recipe = ExportRecipes[data.Recipe]; - inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ - ItemType: ingredient.ItemType, - ItemCount: ingredient.ItemCount * -1 - })); - addMiscItems(inventory, inventoryChanges.MiscItems); - const affiliationMods = []; - if (recipe.syndicateStandingChange) { - const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; - affiliation.Standing += recipe.syndicateStandingChange.value; - affiliationMods.push({ - Tag: recipe.syndicateStandingChange.tag, - Standing: recipe.syndicateStandingChange.value - }); + + if (data.Recipe != "webui") { + const recipe = ExportRecipes[data.Recipe]; + inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ + ItemType: ingredient.ItemType, + ItemCount: ingredient.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); + + if (recipe.syndicateStandingChange) { + const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; + affiliation.Standing += recipe.syndicateStandingChange.value; + affiliationMods.push({ + Tag: recipe.syndicateStandingChange.tag, + Standing: recipe.syndicateStandingChange.value + }); + } } await inventory.save(); diff --git a/src/controllers/custom/gildEquipmentController.ts b/src/controllers/custom/gildEquipmentController.ts deleted file mode 100644 index 46716207..00000000 --- a/src/controllers/custom/gildEquipmentController.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getInventory } from "@/src/services/inventoryService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; -import { RequestHandler } from "express"; - -export const gildEquipmentController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); - const request = req.body as IGildEquipmentRequest; - const inventory = await getInventory(accountId, request.Category); - const weapon = inventory[request.Category].id(request.ItemId); - if (weapon) { - weapon.Features ??= 0; - weapon.Features |= EquipmentFeatures.GILDED; - await inventory.save(); - } - res.end(); -}; - -type IGildEquipmentRequest = { - ItemId: string; - Category: TEquipmentKey; -}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 0834f90b..16359226 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -17,7 +17,6 @@ import { addCurrencyController } from "@/src/controllers/custom/addCurrencyContr import { addItemsController } from "@/src/controllers/custom/addItemsController"; import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController"; import { addXpController } from "@/src/controllers/custom/addXpController"; -import { gildEquipmentController } from "@/src/controllers/custom/gildEquipmentController"; import { importController } from "@/src/controllers/custom/importController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; @@ -43,7 +42,6 @@ customRouter.post("/addCurrency", addCurrencyController); customRouter.post("/addItems", addItemsController); customRouter.post("/addModularEquipment", addModularEquipmentController); customRouter.post("/addXp", addXpController); -customRouter.post("/gildEquipment", gildEquipmentController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); diff --git a/static/webui/script.js b/static/webui/script.js index d55219a0..53dbf126 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1127,11 +1127,10 @@ function disposeOfItems(category, type, count) { function gildEquipment(category, oid) { revalidateAuthz(() => { $.post({ - url: "/custom/gildEquipment?" + window.authz, - contentType: "application/json", + url: "/api/gildWeapon.php?" + window.authz + "&ItemId=" + oid + "&Category=" + category, + contentType: "application/octet-stream", data: JSON.stringify({ - ItemId: oid, - Category: category + Recipe: "webui" }) }).done(function () { updateInventory(); From feb1dd471541089ea3b3e9a5ba2eb3d05d81df42 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:15:27 -0700 Subject: [PATCH 422/776] chore: improve changeDojoRoot (#1522) Using SortId instead of actually changing the component ids. What's strange is that providing/omitting SortId does seem to make a difference in regards to deco positioning, which is presumably what the POST body would be for. I've opted to simply always provide the SortId in hopes that this avoids the need for repositioning entirely. Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/changeDojoRootController.ts | 20 ++++++++----------- src/models/guildModel.ts | 1 + src/routes/api.ts | 1 + src/services/guildService.ts | 1 + src/types/guildTypes.ts | 1 + 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/changeDojoRootController.ts b/src/controllers/api/changeDojoRootController.ts index e28b92d9..1c7cb792 100644 --- a/src/controllers/api/changeDojoRootController.ts +++ b/src/controllers/api/changeDojoRootController.ts @@ -15,6 +15,12 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { return; } + // Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"} + if (req.body) { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId + } + const idToNode: Record = {}; guild.DojoComponents.forEach(x => { idToNode[x._id.toString()] = { @@ -43,23 +49,13 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { newRoot.component.pp = undefined; newRoot.parent = undefined; - // Don't even ask me why this is needed because I don't know either + // Set/update SortId in top-to-bottom order const stack: INode[] = [newRoot]; - let i = 0; - const idMap: Record = {}; while (stack.length != 0) { const top = stack.shift()!; - idMap[top.component._id.toString()] = new Types.ObjectId( - (++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8) - ); + top.component.SortId = new Types.ObjectId(); top.children.forEach(x => stack.push(x)); } - guild.DojoComponents.forEach(x => { - x._id = idMap[x._id.toString()]; - if (x.pi) { - x.pi = idMap[x.pi.toString()]; - } - }); logger.debug("New tree:\n" + treeToString(newRoot)); diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 2d757039..7cd28c6c 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -43,6 +43,7 @@ const dojoLeaderboardEntrySchema = new Schema( ); const dojoComponentSchema = new Schema({ + SortId: Schema.Types.ObjectId, pf: { type: String, required: true }, ppf: String, pi: Schema.Types.ObjectId, diff --git a/src/routes/api.ts b/src/routes/api.ts index d8be0cb3..7dfd562f 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -145,6 +145,7 @@ const apiRouter = express.Router(); apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController); +apiRouter.get("/changeDojoRoot.php", changeDojoRootController); apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index f8e33a5e..3dbddc02 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -147,6 +147,7 @@ export const getDojoClient = async ( if (!componentId || dojoComponent._id.equals(componentId)) { const clientComponent: IDojoComponentClient = { id: toOid(dojoComponent._id), + SortId: toOid(dojoComponent.SortId ?? dojoComponent._id), // always providing a SortId so decos don't need repositioning to reparent pf: dojoComponent.pf, ppf: dojoComponent.ppf, Name: dojoComponent.Name, diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 2502b57e..518e6bcf 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -190,6 +190,7 @@ export interface IDojoComponentDatabase "id" | "SortId" | "pi" | "CompletionTime" | "DestructionTime" | "Decos" | "PaintBot" > { _id: Types.ObjectId; + SortId?: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; CompletionLogPending?: boolean; From c2ed8b40f010ac7403697ee746c020fafdf3040f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:15:54 -0700 Subject: [PATCH 423/776] feat: track EudicoHeists in CompletedJobChains (#1531) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1531 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/missionInventoryUpdateService.ts | 22 +++++++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 2 +- src/types/requestTypes.ts | 7 ++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index b4285d5f..01b5aca5 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1415,7 +1415,7 @@ const inventorySchema = new Schema( //https://warframe.fandom.com/wiki/Heist //ProfitTaker(1-4) Example:"LocationTag": "EudicoHeists", "Jobs":Mission name - CompletedJobChains: [completedJobChainsSchema], + CompletedJobChains: { type: [completedJobChainsSchema], default: undefined }, //Night Wave Challenge SeasonChallengeHistory: [seasonChallengeHistorySchema], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index ab5bdcdb..e0641adf 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -143,6 +143,28 @@ export const addMissionInventoryUpdates = async ( if (inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } + if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) { + // e.g. for Profit-Taker Phase 1: + // JobTier: -6, + // jobId: '/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne_-6_SolarisUnitedHub1_663a71c80000000000000025_EudicoHeists', + // This is sent multiple times, with JobStage starting at 0 and incrementing each time, but only the final upload has GS_SUCCESS. + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [bounty, tier, hub, id, tag] = inventoryUpdates.RewardInfo.jobId.split("_"); + if (tag == "EudicoHeists") { + inventory.CompletedJobChains ??= []; + let chain = inventory.CompletedJobChains.find(x => x.LocationTag == tag); + if (!chain) { + chain = + inventory.CompletedJobChains[ + inventory.CompletedJobChains.push({ LocationTag: tag, Jobs: [] }) - 1 + ]; + } + if (!chain.Jobs.includes(bounty)) { + chain.Jobs.push(bounty); + } + } + } } for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { if (value === undefined) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index fd957a82..c092fb9f 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -292,7 +292,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu RecentVendorPurchases?: IRecentVendorPurchaseClient[]; NodeIntrosCompleted: string[]; GuildId?: IOid; - CompletedJobChains: ICompletedJobChain[]; + CompletedJobChains?: ICompletedJobChain[]; SeasonChallengeHistory: ISeasonChallenge[]; EquippedInstrument?: string; InvasionChainProgress: IInvasionChainProgress[]; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 173d150d..1f00fa45 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -143,6 +143,13 @@ export interface IRewardInfo { PurgatoryRewardQualifications?: string; rewardSeed?: number; periodicMissionTag?: string; + + // for bounties, only EOM_AFK and node are given from above, plus: + JobTier?: string; + jobId?: string; + JobStage?: string; + Q?: boolean; // always false? + CheckpointCounter?: number; // starts at 1, is incremented with each job stage upload, and does not reset when starting a new job } export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; From 0ffcee5faf6e56367843bf7951ba3b5a954b3ac2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:16:06 -0700 Subject: [PATCH 424/776] fix: set deathmark message title to the boss' name (#1533) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1533 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index e0641adf..c9c93e9e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -385,12 +385,12 @@ export const addMissionInventoryUpdates = async ( break; } case "DeathMarks": { - for (const deathMark of value) { - if (!inventory.DeathMarks.find(x => x == deathMark)) { + for (const bossName of value) { + if (inventory.DeathMarks.indexOf(bossName) == -1) { // It's a new death mark; we have to say the line. await createMessage(inventory.accountOwnerId, [ { - sub: "/Lotus/Language/G1Quests/DeathMarkTitle", + sub: bossName, sndr: "/Lotus/Language/G1Quests/DeathMarkSender", msg: "/Lotus/Language/G1Quests/DeathMarkMessage", icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", From d3620c00e2a874f9a4d72fe9489cdb27957597ff Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:16:30 -0700 Subject: [PATCH 425/776] feat: automatically delete death mark messages after 24 hours (#1535) Possibly unfaithful but more faithful than never deleting it at all. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1535 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inboxModel.ts | 10 ++++++---- src/services/missionInventoryUpdateService.ts | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index 85dff3fb..37a7fc5e 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -5,7 +5,7 @@ import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; export interface IMessageClient - extends Omit { + extends Omit { _id?: IOid; date: IMongoDate; startDate?: IMongoDate; @@ -16,6 +16,8 @@ export interface IMessageClient export interface IMessageDatabase extends IMessage { ownerId: Types.ObjectId; date: Date; //created at + attVisualOnly?: boolean; + expiry?: Date; _id: Types.ObjectId; } @@ -30,7 +32,6 @@ export interface IMessage { endDate?: Date; att?: string[]; countedAtt?: ITypeCount[]; - attVisualOnly?: boolean; transmission?: string; arg?: Arg[]; gifts?: IGift[]; @@ -137,14 +138,14 @@ messageSchema.virtual("messageId").get(function (this: IMessageDatabase) { messageSchema.set("toJSON", { virtuals: true, transform(_document, returnedObject) { - delete returnedObject.ownerId; - const messageDatabase = returnedObject as IMessageDatabase; const messageClient = returnedObject as IMessageClient; delete returnedObject._id; delete returnedObject.__v; + delete returnedObject.ownerId; delete returnedObject.attVisualOnly; + delete returnedObject.expiry; messageClient.date = toMongoDate(messageDatabase.date); @@ -157,5 +158,6 @@ messageSchema.set("toJSON", { }); messageSchema.index({ ownerId: 1 }); +messageSchema.index({ expiry: 1 }, { expireAfterSeconds: 0 }); export const Inbox = model("Inbox", messageSchema, "inbox"); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c9c93e9e..5af53893 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -394,10 +394,10 @@ export const addMissionInventoryUpdates = async ( sndr: "/Lotus/Language/G1Quests/DeathMarkSender", msg: "/Lotus/Language/G1Quests/DeathMarkMessage", icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", - highPriority: true + highPriority: true, + expiry: 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 clear if this is correct. } ]); - // TODO: This type of inbox message seems to automatically delete itself. Figure out under which conditions. } } inventory.DeathMarks = value; From a0b61bec128575e9719e1bc29a3fa1d7263a0fed Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:16:54 -0700 Subject: [PATCH 426/776] feat: KIM gifts (#1538) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1538 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index ccf67fc7..08df0676 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,4 +1,4 @@ -import { addEmailItem, getInventory } from "@/src/services/inventoryService"; +import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; @@ -9,7 +9,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const request = JSON.parse(String(req.body)) as SaveDialogueRequest; if ("YearIteration" in request) { - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "DialogueHistory"); if (inventory.DialogueHistory) { inventory.DialogueHistory.YearIteration = request.YearIteration; } else { @@ -22,10 +22,11 @@ export const saveDialogueController: RequestHandler = async (req, res) => { if (!inventory.DialogueHistory) { throw new Error("bad inventory state"); } - if (request.OtherDialogueInfos.length != 0) { + if (request.OtherDialogueInfos.length != 0 || !(request.Data || request.Gift)) { logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); } const inventoryChanges: IInventoryChanges = {}; + const tomorrowAt0Utc = (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; inventory.DialogueHistory.Dialogues ??= []; let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName); if (!dialogue) { @@ -48,31 +49,46 @@ export const saveDialogueController: RequestHandler = async (req, res) => { } dialogue.Rank = request.Rank; dialogue.Chemistry = request.Chemistry; - dialogue.QueuedDialogues = request.QueuedDialogues; - for (const bool of request.Booleans) { - dialogue.Booleans.push(bool); - if (bool == "LizzieShawzin") { - await addEmailItem( - inventory, - "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem", - inventoryChanges - ); + if (request.Data) { + dialogue.QueuedDialogues = request.QueuedDialogues; + for (const bool of request.Booleans) { + dialogue.Booleans.push(bool); + if (bool == "LizzieShawzin") { + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem", + inventoryChanges + ); + } } - } - for (const bool of request.ResetBooleans) { - const index = dialogue.Booleans.findIndex(x => x == bool); - if (index != -1) { - dialogue.Booleans.splice(index, 1); + for (const bool of request.ResetBooleans) { + const index = dialogue.Booleans.findIndex(x => x == bool); + if (index != -1) { + dialogue.Booleans.splice(index, 1); + } } + dialogue.Completed.push(request.Data); + dialogue.AvailableDate = new Date(tomorrowAt0Utc); + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } + }); + } else if (request.Gift) { + const inventoryChanges = updateCurrency(inventory, request.Gift.Cost, false); + const gift = dialogue.Gifts.find(x => x.Item == request.Gift!.Item); + if (gift) { + gift.GiftedQuantity += 1; + } else { + dialogue.Gifts.push({ Item: request.Gift.Item, GiftedQuantity: 1 }); + } + dialogue.AvailableGiftDate = new Date(tomorrowAt0Utc); + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } + }); } - dialogue.Completed.push(request.Data); - const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000; - dialogue.AvailableDate = new Date(tomorrowAt0Utc); - await inventory.save(); - res.json({ - InventoryChanges: inventoryChanges, - AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } - }); } }; @@ -88,9 +104,15 @@ interface SaveCompletedDialogueRequest { Chemistry: number; CompletionType: number; QueuedDialogues: string[]; + Gift?: { + Item: string; + GainedChemistry: number; + Cost: number; + GiftedQuantity: number; + }; Booleans: string[]; ResetBooleans: string[]; - Data: ICompletedDialogue; + Data?: ICompletedDialogue; OtherDialogueInfos: IOtherDialogueInfo[]; // unsure } From c0947b8822ab83477091b5ddc24c9cc2355ce88c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:17:40 -0700 Subject: [PATCH 427/776] chore(webui): use select for "supported syndicate" (#1539) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1539 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/index.html | 3 +-- static/webui/script.js | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/static/webui/index.html b/static/webui/index.html index b567687e..7d3af19c 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -649,7 +649,7 @@
- +
@@ -676,7 +676,6 @@ - diff --git a/static/webui/script.js b/static/webui/script.js index 53dbf126..d6216a08 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -178,9 +178,9 @@ function fetchItemList() { }); const syndicateNone = document.createElement("option"); - syndicateNone.setAttribute("data-key", ""); - syndicateNone.value = loc("cheats_none"); - document.getElementById("datalist-Syndicates").appendChild(syndicateNone); + syndicateNone.textContent = loc("cheats_none"); + document.getElementById("changeSyndicate").innerHTML = ""; + document.getElementById("changeSyndicate").appendChild(syndicateNone); window.archonCrystalUpgrades = data.archonCrystalUpgrades; @@ -264,6 +264,16 @@ function fetchItemList() { }); } else if (type == "uniqueLevelCaps") { uniqueLevelCaps = items; + } else if (type == "Syndicates") { + items.forEach(item => { + if (item.uniqueName.startsWith("RadioLegion")) { + item.name += " (" + item.uniqueName + ")"; + } + const option = document.createElement("option"); + option.value = item.uniqueName; + option.textContent = item.name; + document.getElementById("changeSyndicate").appendChild(option); + }); } else { const nameSet = new Set(); items.forEach(item => { @@ -277,9 +287,6 @@ function fetchItemList() { item.name += " " + loc("code_badItem"); } } - if (type == "Syndicates" && item.uniqueName.startsWith("RadioLegion")) { - item.name += " (" + item.uniqueName + ")"; - } if (type == "ModularParts") { const supportedModularParts = [ "LWPT_HB_DECK", @@ -823,10 +830,7 @@ function updateInventory() { single.loadRoute("/webui/inventory"); } } - document.getElementById("changeSyndicate").value = - [...document.querySelectorAll("#datalist-Syndicates option")].find( - option => option.getAttribute("data-key") === (data.SupportedSyndicate ?? "") - )?.value ?? loc("cheats_none"); + document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? ""; }); }); } @@ -1505,7 +1509,7 @@ function doImport() { } function doChangeSupportedSyndicate() { - const uniqueName = getKey(document.getElementById("changeSyndicate")); + const uniqueName = document.getElementById("changeSyndicate").value; revalidateAuthz(() => { $.get("/api/setSupportedSyndicate.php?" + window.authz + "&syndicate=" + uniqueName).done(function () { From e784b2dfb8772da7a05d1a5e33f1664f4ae0750d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:05:35 +0200 Subject: [PATCH 428/776] chore: fix typo --- src/controllers/dynamic/worldStateController.ts | 2 +- src/helpers/{worlstateHelper.ts => worldStateHelper.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/helpers/{worlstateHelper.ts => worldStateHelper.ts} (100%) diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 3cf397ba..f9add2dd 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -19,7 +19,7 @@ import { sortieBossToFaction, sortieFactionToFactionIndexes, sortieFactionToSystemIndexes -} from "@/src/helpers/worlstateHelper"; +} from "@/src/helpers/worldStateHelper"; export const worldStateController: RequestHandler = (req, res) => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); diff --git a/src/helpers/worlstateHelper.ts b/src/helpers/worldStateHelper.ts similarity index 100% rename from src/helpers/worlstateHelper.ts rename to src/helpers/worldStateHelper.ts From 3f47f89b568de488cb4a92d4aeb95a5989005c1d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:40:57 -0700 Subject: [PATCH 429/776] chore: update PE+ (#1546) and make use of some of the new data Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1546 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++-- package.json | 2 +- .../dynamic/worldStateController.ts | 5 +- src/helpers/worldStateHelper.ts | 46 ------------------- src/services/missionInventoryUpdateService.ts | 25 +++++----- 5 files changed, 21 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1fe1acd..46cdebbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.52", + "warframe-public-export-plus": "^0.5.53", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.52", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.52.tgz", - "integrity": "sha512-mJyQbTFMDwgBSkhUYJzcfJg9qrMTrL1pyZuAxV/Dov68xUikK5zigQSYM3ZkKYbhwBtg0Bx/+7q9GAmPzGaRhA==" + "version": "0.5.53", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.53.tgz", + "integrity": "sha512-FjYeCJ5OxvPWyETnV33YOeX7weVVeMy451RY7uewwSvRbSNFTDhmhvbrLhfwykulUX4RPakfZr8nO0S0a6lGCA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 899947c7..f9f680b6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0", - "warframe-public-export-plus": "^0.5.52", + "warframe-public-export-plus": "^0.5.53", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index f9add2dd..dcde3fdf 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -9,11 +9,10 @@ import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; -import { ExportNightwave, ExportRegions } from "warframe-public-export-plus"; +import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; import { EPOCH, getSortieTime, - missionTags, sortieBosses, sortieBossNode, sortieBossToFaction, @@ -269,7 +268,7 @@ export const worldStateController: RequestHandler = (req, res) => { } } - const missionType = missionTags[missionIndex]; + const missionType = eMissionType[missionIndex].tag; if (missionTypes.has(missionType)) { i--; diff --git a/src/helpers/worldStateHelper.ts b/src/helpers/worldStateHelper.ts index 4501d4d1..4de03e03 100644 --- a/src/helpers/worldStateHelper.ts +++ b/src/helpers/worldStateHelper.ts @@ -1,49 +1,3 @@ -export const missionTags = [ - "MT_ASSASSINATION", - "MT_EXTERMINATION", - "MT_SURVIVAL", - "MT_RESCUE", - "MT_SABOTAGE", - "MT_CAPTURE", - "MT_COUNTER_INTEL", - "MT_INTEL", - "MT_DEFENSE", - "MT_MOBILE_DEFENSE", - "MT_PVP", - "MT_MASTERY", - "MT_RECOVERY", - "MT_TERRITORY", - "MT_RETRIEVAL", - "MT_HIVE", - "MT_SALVAGE", - "MT_EXCAVATE", - "MT_RAID", - "MT_PURGE", - "MT_GENERIC", - "MT_PURIFY", - "MT_ARENA", - "MT_JUNCTION", - "MT_PURSUIT", - "MT_RACE", - "MT_ASSAULT", - "MT_EVACUATION", - "MT_LANDSCAPE", - "MT_RESOURCE_THEFT", - "MT_ENDLESS_EXTERMINATION", - "MT_ENDLESS_DUVIRI", - "MT_RAILJACK", - "MT_ARTIFACT", - "MT_CORRUPTION", - "MT_VOID_CASCADE", - "MT_ARMAGEDDON", - "MT_VAULTS", - "MT_ALCHEMY", - "MT_ASCENSION", - "MT_ENDLESS_CAPTURE", - "MT_OFFERING", - "MT_PVPVE" -]; - export const sortieBosses = [ "SORTIE_BOSS_HYENA", "SORTIE_BOSS_KELA", diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 5af53893..598c0fc6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -619,16 +619,12 @@ export const addMissionRewards = async ( if (strippedItems) { for (const si of strippedItems) { - const droptable = ExportEnemies.droptables[si.DropTable]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!droptable) { - logger.error(`unknown droptable ${si.DropTable}`); - } else { - const modsPool = droptable[0].items; - const blueprintsPool = (droptable.length > 1 ? droptable[1] : droptable[0]).items; - if (si.DROP_MOD) { + const droptables = ExportEnemies.droptables[si.DropTable] ?? []; + if (si.DROP_MOD) { + const modDroptable = droptables.find(x => x.type == "mod"); + if (modDroptable) { for (let i = 0; i != si.DROP_MOD.length; ++i) { - const reward = getRandomReward(modsPool)!; + const reward = getRandomReward(modDroptable.items)!; logger.debug(`stripped droptable (mods pool) rolled`, reward); await addItem(inventory, reward.type); MissionRewards.push({ @@ -637,10 +633,15 @@ export const addMissionRewards = async ( FromEnemyCache: true // to show "identified" }); } + } else { + logger.error(`unknown droptable ${si.DropTable} for DROP_MOD`); } - if (si.DROP_BLUEPRINT) { + } + if (si.DROP_BLUEPRINT) { + const blueprintDroptable = droptables.find(x => x.type == "blueprint"); + if (blueprintDroptable) { for (let i = 0; i != si.DROP_BLUEPRINT.length; ++i) { - const reward = getRandomReward(blueprintsPool)!; + const reward = getRandomReward(blueprintDroptable.items)!; logger.debug(`stripped droptable (blueprints pool) rolled`, reward); await addItem(inventory, reward.type); MissionRewards.push({ @@ -649,6 +650,8 @@ export const addMissionRewards = async ( FromEnemyCache: true // to show "identified" }); } + } else { + logger.error(`unknown droptable ${si.DropTable} for DROP_BLUEPRINT`); } } } From fc3ef3a126dcae8c3df864c6556599cf20e86bd0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:54:29 -0700 Subject: [PATCH 430/776] fix: use wagerTier for The Index rewards (#1545) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1545 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 598c0fc6..9ca788af 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -51,10 +51,13 @@ import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -const getRotations = (rotationCount: number): number[] => { +const getRotations = (rotationCount: number, tierOverride: number | undefined): number[] => { if (rotationCount === 0) return [0]; - const rotationPattern = [0, 0, 1, 2]; // A, A, B, C + const rotationPattern = + tierOverride === undefined + ? [0, 0, 1, 2] // A, A, B, C + : [tierOverride]; const rotatedValues = []; for (let i = 0; i < rotationCount; i++) { @@ -518,6 +521,7 @@ interface AddMissionRewardsReturnType { export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, { + wagerTier: wagerTier, Nemesis: nemesis, RewardInfo: rewardInfo, LevelKeyName: levelKeyName, @@ -534,7 +538,7 @@ export const addMissionRewards = async ( } //TODO: check double reward merging - const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo); + const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; @@ -798,7 +802,7 @@ function getLevelCreditRewards(node: IRegion): number { //TODO: get dark sektor fixed credit rewards and railjack bonus } -function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] { +function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] { const drops: IMissionReward[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; @@ -815,7 +819,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] { } } else { const rotationCount = RewardInfo.rewardQualifications?.length || 0; - rotations = getRotations(rotationCount); + rotations = getRotations(rotationCount, tierOverride); } rewardManifests .map(name => ExportRewards[name]) From b308b91f447ff123700d7c6c742f54562895cd1d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:54:43 -0700 Subject: [PATCH 431/776] chore: remove typescript version limit (#1547) This is no longer needed now that the eslint stuff is up-to-date enough. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1547 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46cdebbe..8a09d9ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "ncp": "^2.0.0", - "typescript": ">=5.5 <5.6.0", + "typescript": "^5.5", "warframe-public-export-plus": "^0.5.53", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", @@ -3720,9 +3720,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index f9f680b6..77358348 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "mongoose": "^8.11.0", "morgan": "^1.10.0", "ncp": "^2.0.0", - "typescript": ">=5.5 <5.6.0", + "typescript": "^5.5", "warframe-public-export-plus": "^0.5.53", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", From 9f727789ca287392145e08d7cc62c8cf9b4a8373 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:54:48 -0700 Subject: [PATCH 432/776] chore: split worldState stuff into types & service (#1548) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1548 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../dynamic/worldStateController.ts | 608 +----------------- src/helpers/worldStateHelper.ts | 84 --- src/services/worldStateService.ts | 561 ++++++++++++++++ src/types/worldStateTypes.ts | 122 ++++ 4 files changed, 685 insertions(+), 690 deletions(-) delete mode 100644 src/helpers/worldStateHelper.ts create mode 100644 src/services/worldStateService.ts create mode 100644 src/types/worldStateTypes.ts diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index dcde3fdf..89335497 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -1,610 +1,6 @@ import { RequestHandler } from "express"; -import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; -import static1999FallDays from "@/static/fixed_responses/worldState/1999_fall_days.json"; -import static1999SpringDays from "@/static/fixed_responses/worldState/1999_spring_days.json"; -import static1999SummerDays from "@/static/fixed_responses/worldState/1999_summer_days.json"; -import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winter_days.json"; -import { buildConfig } from "@/src/services/buildConfigService"; -import { IMongoDate, IOid } from "@/src/types/commonTypes"; -import { unixTimesInMs } from "@/src/constants/timeConstants"; -import { config } from "@/src/services/configService"; -import { CRng } from "@/src/services/rngService"; -import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { - EPOCH, - getSortieTime, - sortieBosses, - sortieBossNode, - sortieBossToFaction, - sortieFactionToFactionIndexes, - sortieFactionToSystemIndexes -} from "@/src/helpers/worldStateHelper"; +import { getWorldState } from "@/src/services/worldStateService"; export const worldStateController: RequestHandler = (req, res) => { - const day = Math.trunc((Date.now() - EPOCH) / 86400000); - const week = Math.trunc(day / 7); - const weekStart = EPOCH + week * 604800000; - const weekEnd = weekStart + 604800000; - - const worldState: IWorldState = { - BuildLabel: - typeof req.query.buildLabel == "string" - ? req.query.buildLabel.split(" ").join("+") - : buildConfig.buildLabel, - Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), - Goals: [], - GlobalUpgrades: [], - Sorties: [], - LiteSorties: [], - EndlessXpChoices: [], - SeasonInfo: { - Activation: { $date: { $numberLong: "1715796000000" } }, - Expiry: { $date: { $numberLong: "2000000000000" } }, - AffiliationTag: "RadioLegionIntermission12Syndicate", - Season: 14, - Phase: 0, - Params: "", - ActiveChallenges: [ - getSeasonDailyChallenge(day - 2), - getSeasonDailyChallenge(day - 1), - getSeasonDailyChallenge(day - 0), - getSeasonWeeklyChallenge(week, 0), - getSeasonWeeklyChallenge(week, 1), - getSeasonWeeklyHardChallenge(week, 2), - getSeasonWeeklyHardChallenge(week, 3), - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: - "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) - }, - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) - }, - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) - } - ] - }, - ...staticWorldState - }; - - if (config.worldState?.starDays) { - worldState.Goals.push({ - _id: { $oid: "67a4dcce2a198564d62e1647" }, - Activation: { $date: { $numberLong: "1738868400000" } }, - Expiry: { $date: { $numberLong: "2000000000000" } }, - Count: 0, - Goal: 0, - Success: 0, - Personal: true, - Desc: "/Lotus/Language/Events/ValentinesFortunaName", - ToolTip: "/Lotus/Language/Events/ValentinesFortunaName", - Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png", - Tag: "FortunaValentines", - Node: "SolarisUnitedHub1" - }); - } - - // Elite Sanctuary Onslaught cycling every week - worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful - - // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - const bountyCycle = Math.trunc(Date.now() / 9000000); - const bountyCycleStart = bountyCycle * 9000000; - const bountyCycleEnd = bountyCycleStart + 9000000; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "ZarimanSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "EntratiLabSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "HexSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - - if (config.worldState?.creditBoost) { - worldState.GlobalUpgrades.push({ - _id: { $oid: "5b23106f283a555109666672" }, - Activation: { $date: { $numberLong: "1740164400000" } }, - ExpiryDate: { $date: { $numberLong: "2000000000000" } }, - UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT", - OperationType: "MULTIPLY", - Value: 2, - LocalizeTag: "", - LocalizeDescTag: "" - }); - } - if (config.worldState?.affinityBoost) { - worldState.GlobalUpgrades.push({ - _id: { $oid: "5b23106f283a555109666673" }, - Activation: { $date: { $numberLong: "1740164400000" } }, - ExpiryDate: { $date: { $numberLong: "2000000000000" } }, - UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT", - OperationType: "MULTIPLY", - Value: 2, - LocalizeTag: "", - LocalizeDescTag: "" - }); - } - if (config.worldState?.resourceBoost) { - worldState.GlobalUpgrades.push({ - _id: { $oid: "5b23106f283a555109666674" }, - Activation: { $date: { $numberLong: "1740164400000" } }, - ExpiryDate: { $date: { $numberLong: "2000000000000" } }, - UpgradeType: "GAMEPLAY_PICKUP_AMOUNT", - OperationType: "MULTIPLY", - Value: 2, - LocalizeTag: "", - LocalizeDescTag: "" - }); - } - - // Sortie cycling every day - { - let genDay; - let dayStart; - let dayEnd; - const sortieRolloverToday = getSortieTime(day); - if (Date.now() < sortieRolloverToday) { - // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`. - genDay = day - 1; - dayStart = getSortieTime(genDay); - dayEnd = sortieRolloverToday; - } else { - // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`. - genDay = day; - dayStart = sortieRolloverToday; - dayEnd = getSortieTime(day + 1); - } - - const rng = new CRng(genDay); - - const boss = rng.randomElement(sortieBosses); - - const modifiers = [ - "SORTIE_MODIFIER_LOW_ENERGY", - "SORTIE_MODIFIER_IMPACT", - "SORTIE_MODIFIER_SLASH", - "SORTIE_MODIFIER_PUNCTURE", - "SORTIE_MODIFIER_EXIMUS", - "SORTIE_MODIFIER_MAGNETIC", - "SORTIE_MODIFIER_CORROSIVE", - "SORTIE_MODIFIER_VIRAL", - "SORTIE_MODIFIER_ELECTRICITY", - "SORTIE_MODIFIER_RADIATION", - "SORTIE_MODIFIER_GAS", - "SORTIE_MODIFIER_FIRE", - "SORTIE_MODIFIER_EXPLOSION", - "SORTIE_MODIFIER_FREEZE", - "SORTIE_MODIFIER_TOXIN", - "SORTIE_MODIFIER_POISON", - "SORTIE_MODIFIER_HAZARD_RADIATION", - "SORTIE_MODIFIER_HAZARD_MAGNETIC", - "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest - "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon - "SORTIE_MODIFIER_HAZARD_ICE", - "SORTIE_MODIFIER_HAZARD_COLD", - "SORTIE_MODIFIER_SECONDARY_ONLY", - "SORTIE_MODIFIER_SHOTGUN_ONLY", - "SORTIE_MODIFIER_SNIPER_ONLY", - "SORTIE_MODIFIER_RIFLE_ONLY", - "SORTIE_MODIFIER_MELEE_ONLY", - "SORTIE_MODIFIER_BOW_ONLY" - ]; - - if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); - if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); - - const nodes: string[] = []; - const availableMissionIndexes: number[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && - sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && - value.name.indexOf("Archwing") == -1 && - value.missionIndex != 0 && // Exclude MT_ASSASSINATION - value.missionIndex != 5 && // Exclude MT_CAPTURE - value.missionIndex != 21 && // Exclude MT_PURIFY - value.missionIndex != 23 && // Exclude MT_JUNCTION - value.missionIndex <= 28 - ) { - if (!availableMissionIndexes.includes(value.missionIndex)) { - availableMissionIndexes.push(value.missionIndex); - } - nodes.push(key); - } - } - - const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; - const missionTypes = new Set(); - - for (let i = 0; i < 3; i++) { - const randomIndex = rng.randomInt(0, nodes.length - 1); - const node = nodes[randomIndex]; - let missionIndex = ExportRegions[node].missionIndex; - - if ( - !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions - missionIndex != 28 && - rng.randomInt(0, 2) == 2 - ) { - missionIndex = rng.randomElement(availableMissionIndexes); - } - - if (i == 2 && rng.randomInt(0, 2) == 2) { - const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); - const modifierType = rng.randomElement(filteredModifiers); - - if (boss == "SORTIE_BOSS_PHORID") { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); - nodes.splice(randomIndex, 1); - continue; - } else if (sortieBossNode[boss]) { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); - continue; - } - } - - const missionType = eMissionType[missionIndex].tag; - - if (missionTypes.has(missionType)) { - i--; - continue; - } - - const filteredModifiers = - missionType === "MT_TERRITORY" - ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") - : modifiers; - - const modifierType = rng.randomElement(filteredModifiers); - - selectedNodes.push({ missionType, modifierType, node }); - nodes.splice(randomIndex, 1); - missionTypes.add(missionType); - } - - worldState.Sorties.push({ - _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, - Activation: { $date: { $numberLong: dayStart.toString() } }, - Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", - Seed: genDay, - Boss: boss, - Variants: selectedNodes - }); - } - - // Archon Hunt cycling every week - { - const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; - const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; - const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth - - const nodes: string[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - value.systemIndex === systemIndex && - value.factionIndex !== undefined && - value.factionIndex < 2 && - value.name.indexOf("Archwing") == -1 && - value.missionIndex != 0 // Exclude MT_ASSASSINATION - ) { - nodes.push(key); - } - } - - const rng = new CRng(week); - const firstNodeIndex = rng.randomInt(0, nodes.length - 1); - const firstNode = nodes[firstNodeIndex]; - nodes.splice(firstNodeIndex, 1); - worldState.LiteSorties.push({ - _id: { - $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" - }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", - Seed: week, - Boss: boss, - Missions: [ - { - missionType: rng.randomElement([ - "MT_INTEL", - "MT_MOBILE_DEFENSE", - "MT_EXTERMINATION", - "MT_SABOTAGE", - "MT_RESCUE" - ]), - node: firstNode - }, - { - missionType: rng.randomElement([ - "MT_DEFENSE", - "MT_TERRITORY", - "MT_ARTIFACT", - "MT_EXCAVATE", - "MT_SURVIVAL" - ]), - node: rng.randomElement(nodes) - }, - { - missionType: "MT_ASSASSINATION", - node: showdownNode - } - ] - }); - } - - // Circuit choices cycling every week - worldState.EndlessXpChoices.push({ - Category: "EXC_NORMAL", - Choices: [ - ["Nidus", "Octavia", "Harrow"], - ["Gara", "Khora", "Revenant"], - ["Garuda", "Baruuk", "Hildryn"], - ["Excalibur", "Trinity", "Ember"], - ["Loki", "Mag", "Rhino"], - ["Ash", "Frost", "Nyx"], - ["Saryn", "Vauban", "Nova"], - ["Nekros", "Valkyr", "Oberon"], - ["Hydroid", "Mirage", "Limbo"], - ["Mesa", "Chroma", "Atlas"], - ["Ivara", "Inaros", "Titania"] - ][week % 12] - }); - worldState.EndlessXpChoices.push({ - Category: "EXC_HARD", - Choices: [ - ["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"], - ["Bo", "Latron", "Furis", "Furax", "Strun"], - ["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"], - ["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"], - ["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"], - ["Zylok", "Sibear", "Dread", "Despair", "Hate"], - ["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"], - ["Braton", "Lato", "Skana", "Paris", "Kunai"] - ][week % 8] - }); - - // 1999 Calendar Season cycling every week + YearIteration every 4 weeks - worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } }; - worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } }; - worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4]; - worldState.KnownCalendarSeasons[0].Days = [ - static1999WinterDays, - static1999SpringDays, - static1999SummerDays, - static1999FallDays - ][week % 4]; - worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4); - - // Sentient Anomaly cycling every 30 minutes - const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); - const tmp = { - cavabegin: "1690761600", - PurchasePlatformLockEnabled: true, - tcsn: true, - pgr: { - ts: "1732572900", - en: "CUSTOM DECALS @ ZEVILA", - fr: "DECALS CUSTOM @ ZEVILA", - it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA", - de: "AUFKLEBER NACH WUNSCH @ ZEVILA", - es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA", - pt: "DECALQUES PERSONALIZADOS NA ZEVILA", - ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа", - pl: "NOWE NAKLEJKI @ ZEVILA", - uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА", - tr: "ÖZEL ÇIKARTMALAR @ ZEVILA", - ja: "カスタムデカール @ ゼビラ", - zh: "定制贴花认准泽威拉", - ko: "커스텀 데칼 @ ZEVILA", - tc: "自訂貼花 @ ZEVILA", - th: "รูปลอกสั่งทำที่ ZEVILA" - }, - ennnd: true, - mbrt: true, - sfn: [550, 553, 554, 555][halfHour % 4] - }; - worldState.Tmp = JSON.stringify(tmp); - - res.json(worldState); -}; - -interface IWorldState { - Version: number; // for goals - BuildLabel: string; - Time: number; - Goals: IGoal[]; - SyndicateMissions: ISyndicateMission[]; - GlobalUpgrades: IGlobalUpgrade[]; - Sorties: ISortie[]; - LiteSorties: ILiteSortie[]; - NodeOverrides: INodeOverride[]; - EndlessXpChoices: IEndlessXpChoice[]; - SeasonInfo: { - Activation: IMongoDate; - Expiry: IMongoDate; - AffiliationTag: string; - Season: number; - Phase: number; - Params: string; - ActiveChallenges: ISeasonChallenge[]; - }; - KnownCalendarSeasons: ICalendarSeason[]; - Tmp?: string; -} - -interface IGoal { - _id: IOid; - Activation: IMongoDate; - Expiry: IMongoDate; - Count: number; - Goal: number; - Success: number; - Personal: boolean; - Desc: string; - ToolTip: string; - Icon: string; - Tag: string; - Node: string; -} - -interface ISyndicateMission { - _id: IOid; - Activation: IMongoDate; - Expiry: IMongoDate; - Tag: string; - Seed: number; - Nodes: string[]; -} - -interface IGlobalUpgrade { - _id: IOid; - Activation: IMongoDate; - ExpiryDate: IMongoDate; - UpgradeType: string; - OperationType: string; - Value: number; - LocalizeTag: string; - LocalizeDescTag: string; -} - -interface INodeOverride { - _id: IOid; - Activation?: IMongoDate; - Expiry?: IMongoDate; - Node: string; - Hide?: boolean; - Seed?: number; - LevelOverride?: string; - Faction?: string; - CustomNpcEncounters?: string; -} - -interface ISortie { - _id: IOid; - Activation: IMongoDate; - Expiry: IMongoDate; - Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards"; - Seed: number; - Boss: string; - Variants: { - missionType: string; - modifierType: string; - node: string; - }[]; -} - -interface ILiteSortie { - _id: IOid; - Activation: IMongoDate; - Expiry: IMongoDate; - Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; - Seed: number; - Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL" - Missions: { - missionType: string; - node: string; - }[]; -} - -interface IEndlessXpChoice { - Category: string; - Choices: string[]; -} - -interface ISeasonChallenge { - _id: IOid; - Daily?: boolean; - Activation: IMongoDate; - Expiry: IMongoDate; - Challenge: string; -} - -interface ICalendarSeason { - Activation: IMongoDate; - Expiry: IMongoDate; - Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL" - Days: { - day: number; - }[]; - YearIteration: number; -} - -const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => - x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") -); - -const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { - const dayStart = EPOCH + day * 86400000; - const dayEnd = EPOCH + (day + 3) * 86400000; - const rng = new CRng(day); - return { - _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, - Daily: true, - Activation: { $date: { $numberLong: dayStart.toString() } }, - Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Challenge: rng.randomElement(dailyChallenges) - }; -}; - -const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter( - x => - x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && - !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") -); - -const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => { - const weekStart = EPOCH + week * 604800000; - const weekEnd = weekStart + 604800000; - const challengeId = week * 7 + id; - const rng = new CRng(challengeId); - return { - _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: rng.randomElement(weeklyChallenges) - }; -}; - -const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x => - x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") -); - -const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => { - const weekStart = EPOCH + week * 604800000; - const weekEnd = weekStart + 604800000; - const challengeId = week * 7 + id; - const rng = new CRng(challengeId); - return { - _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: rng.randomElement(weeklyHardChallenges) - }; + res.json(getWorldState(req.query.buildLabel as string | undefined)); }; diff --git a/src/helpers/worldStateHelper.ts b/src/helpers/worldStateHelper.ts deleted file mode 100644 index 4de03e03..00000000 --- a/src/helpers/worldStateHelper.ts +++ /dev/null @@ -1,84 +0,0 @@ -export const sortieBosses = [ - "SORTIE_BOSS_HYENA", - "SORTIE_BOSS_KELA", - "SORTIE_BOSS_VOR", - "SORTIE_BOSS_RUK", - "SORTIE_BOSS_HEK", - "SORTIE_BOSS_KRIL", - "SORTIE_BOSS_TYL", - "SORTIE_BOSS_JACKAL", - "SORTIE_BOSS_ALAD", - "SORTIE_BOSS_AMBULAS", - "SORTIE_BOSS_NEF", - "SORTIE_BOSS_RAPTOR", - "SORTIE_BOSS_PHORID", - "SORTIE_BOSS_LEPHANTIS", - "SORTIE_BOSS_INFALAD", - "SORTIE_BOSS_CORRUPTED_VOR" -]; - -export const sortieBossToFaction: Record = { - SORTIE_BOSS_HYENA: "FC_CORPUS", - SORTIE_BOSS_KELA: "FC_GRINEER", - SORTIE_BOSS_VOR: "FC_GRINEER", - SORTIE_BOSS_RUK: "FC_GRINEER", - SORTIE_BOSS_HEK: "FC_GRINEER", - SORTIE_BOSS_KRIL: "FC_GRINEER", - SORTIE_BOSS_TYL: "FC_GRINEER", - SORTIE_BOSS_JACKAL: "FC_CORPUS", - SORTIE_BOSS_ALAD: "FC_CORPUS", - SORTIE_BOSS_AMBULAS: "FC_CORPUS", - SORTIE_BOSS_NEF: "FC_CORPUS", - SORTIE_BOSS_RAPTOR: "FC_CORPUS", - SORTIE_BOSS_PHORID: "FC_INFESTATION", - SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION", - SORTIE_BOSS_INFALAD: "FC_INFESTATION", - SORTIE_BOSS_CORRUPTED_VOR: "FC_CORRUPTED" -}; - -export const sortieFactionToSystemIndexes: Record = { - FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18], - FC_CORPUS: [1, 4, 7, 8, 12, 15], - FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15], - FC_CORRUPTED: [14] -}; - -export const sortieFactionToFactionIndexes: Record = { - FC_GRINEER: [0], - FC_CORPUS: [1], - FC_INFESTATION: [0, 1, 2], - FC_CORRUPTED: [3] -}; - -export const sortieBossNode: Record = { - SORTIE_BOSS_HYENA: "SolNode127", - SORTIE_BOSS_KELA: "SolNode193", - SORTIE_BOSS_VOR: "SolNode108", - SORTIE_BOSS_RUK: "SolNode32", - SORTIE_BOSS_HEK: "SolNode24", - SORTIE_BOSS_KRIL: "SolNode99", - SORTIE_BOSS_TYL: "SolNode105", - SORTIE_BOSS_JACKAL: "SolNode104", - SORTIE_BOSS_ALAD: "SolNode53", - SORTIE_BOSS_AMBULAS: "SolNode51", - SORTIE_BOSS_NEF: "SettlementNode20", - SORTIE_BOSS_RAPTOR: "SolNode210", - SORTIE_BOSS_LEPHANTIS: "SolNode712", - SORTIE_BOSS_INFALAD: "SolNode705" -}; - -export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 - -export const getSortieTime = (day: number): number => { - const dayStart = EPOCH + day * 86400000; - const date = new Date(dayStart); - date.setUTCHours(12); - const isDst = new Intl.DateTimeFormat("en-US", { - timeZone: "America/Toronto", - timeZoneName: "short" - }) - .formatToParts(date) - .find(part => part.type === "timeZoneName")! - .value.includes("DT"); - return dayStart + (isDst ? 16 : 17) * 3600000; -}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts new file mode 100644 index 00000000..11259586 --- /dev/null +++ b/src/services/worldStateService.ts @@ -0,0 +1,561 @@ +import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; +import static1999FallDays from "@/static/fixed_responses/worldState/1999_fall_days.json"; +import static1999SpringDays from "@/static/fixed_responses/worldState/1999_spring_days.json"; +import static1999SummerDays from "@/static/fixed_responses/worldState/1999_summer_days.json"; +import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winter_days.json"; +import { buildConfig } from "@/src/services/buildConfigService"; +import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { config } from "@/src/services/configService"; +import { CRng } from "@/src/services/rngService"; +import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; +import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; + +const sortieBosses = [ + "SORTIE_BOSS_HYENA", + "SORTIE_BOSS_KELA", + "SORTIE_BOSS_VOR", + "SORTIE_BOSS_RUK", + "SORTIE_BOSS_HEK", + "SORTIE_BOSS_KRIL", + "SORTIE_BOSS_TYL", + "SORTIE_BOSS_JACKAL", + "SORTIE_BOSS_ALAD", + "SORTIE_BOSS_AMBULAS", + "SORTIE_BOSS_NEF", + "SORTIE_BOSS_RAPTOR", + "SORTIE_BOSS_PHORID", + "SORTIE_BOSS_LEPHANTIS", + "SORTIE_BOSS_INFALAD", + "SORTIE_BOSS_CORRUPTED_VOR" +]; + +const sortieBossToFaction: Record = { + SORTIE_BOSS_HYENA: "FC_CORPUS", + SORTIE_BOSS_KELA: "FC_GRINEER", + SORTIE_BOSS_VOR: "FC_GRINEER", + SORTIE_BOSS_RUK: "FC_GRINEER", + SORTIE_BOSS_HEK: "FC_GRINEER", + SORTIE_BOSS_KRIL: "FC_GRINEER", + SORTIE_BOSS_TYL: "FC_GRINEER", + SORTIE_BOSS_JACKAL: "FC_CORPUS", + SORTIE_BOSS_ALAD: "FC_CORPUS", + SORTIE_BOSS_AMBULAS: "FC_CORPUS", + SORTIE_BOSS_NEF: "FC_CORPUS", + SORTIE_BOSS_RAPTOR: "FC_CORPUS", + SORTIE_BOSS_PHORID: "FC_INFESTATION", + SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION", + SORTIE_BOSS_INFALAD: "FC_INFESTATION", + SORTIE_BOSS_CORRUPTED_VOR: "FC_CORRUPTED" +}; + +const sortieFactionToSystemIndexes: Record = { + FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18], + FC_CORPUS: [1, 4, 7, 8, 12, 15], + FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15], + FC_CORRUPTED: [14] +}; + +const sortieFactionToFactionIndexes: Record = { + FC_GRINEER: [0], + FC_CORPUS: [1], + FC_INFESTATION: [0, 1, 2], + FC_CORRUPTED: [3] +}; + +const sortieBossNode: Record = { + SORTIE_BOSS_HYENA: "SolNode127", + SORTIE_BOSS_KELA: "SolNode193", + SORTIE_BOSS_VOR: "SolNode108", + SORTIE_BOSS_RUK: "SolNode32", + SORTIE_BOSS_HEK: "SolNode24", + SORTIE_BOSS_KRIL: "SolNode99", + SORTIE_BOSS_TYL: "SolNode105", + SORTIE_BOSS_JACKAL: "SolNode104", + SORTIE_BOSS_ALAD: "SolNode53", + SORTIE_BOSS_AMBULAS: "SolNode51", + SORTIE_BOSS_NEF: "SettlementNode20", + SORTIE_BOSS_RAPTOR: "SolNode210", + SORTIE_BOSS_LEPHANTIS: "SolNode712", + SORTIE_BOSS_INFALAD: "SolNode705" +}; + +const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 + +const getSortieTime = (day: number): number => { + const dayStart = EPOCH + day * 86400000; + const date = new Date(dayStart); + date.setUTCHours(12); + const isDst = new Intl.DateTimeFormat("en-US", { + timeZone: "America/Toronto", + timeZoneName: "short" + }) + .formatToParts(date) + .find(part => part.type === "timeZoneName")! + .value.includes("DT"); + return dayStart + (isDst ? 16 : 17) * 3600000; +}; + +const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") +); + +const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { + const dayStart = EPOCH + day * 86400000; + const dayEnd = EPOCH + (day + 3) * 86400000; + const rng = new CRng(day); + return { + _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, + Daily: true, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Challenge: rng.randomElement(dailyChallenges) + }; +}; + +const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter( + x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && + !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") +); + +const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new CRng(challengeId); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(weeklyChallenges) + }; +}; + +const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") +); + +const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new CRng(challengeId); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(weeklyHardChallenges) + }; +}; + +export const getWorldState = (buildLabel?: string): IWorldState => { + const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + + const worldState: IWorldState = { + BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, + Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), + Goals: [], + GlobalUpgrades: [], + Sorties: [], + LiteSorties: [], + EndlessXpChoices: [], + SeasonInfo: { + Activation: { $date: { $numberLong: "1715796000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + AffiliationTag: "RadioLegionIntermission12Syndicate", + Season: 14, + Phase: 0, + Params: "", + ActiveChallenges: [ + getSeasonDailyChallenge(day - 2), + getSeasonDailyChallenge(day - 1), + getSeasonDailyChallenge(day - 0), + getSeasonWeeklyChallenge(week, 0), + getSeasonWeeklyChallenge(week, 1), + getSeasonWeeklyHardChallenge(week, 2), + getSeasonWeeklyHardChallenge(week, 3), + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: + "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }, + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }, + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + } + ] + }, + ...staticWorldState + }; + + if (config.worldState?.starDays) { + worldState.Goals.push({ + _id: { $oid: "67a4dcce2a198564d62e1647" }, + Activation: { $date: { $numberLong: "1738868400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + Desc: "/Lotus/Language/Events/ValentinesFortunaName", + ToolTip: "/Lotus/Language/Events/ValentinesFortunaName", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png", + Tag: "FortunaValentines", + Node: "SolarisUnitedHub1" + }); + } + + // Elite Sanctuary Onslaught cycling every week + worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful + + // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation + const bountyCycle = Math.trunc(Date.now() / 9000000); + const bountyCycleStart = bountyCycle * 9000000; + const bountyCycleEnd = bountyCycleStart + 9000000; + worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "ZarimanSyndicate", + Seed: bountyCycle, + Nodes: [] + }; + worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "EntratiLabSyndicate", + Seed: bountyCycle, + Nodes: [] + }; + worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "HexSyndicate", + Seed: bountyCycle, + Nodes: [] + }; + + if (config.worldState?.creditBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666672" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + if (config.worldState?.affinityBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666673" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + if (config.worldState?.resourceBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666674" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_PICKUP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + + // Sortie cycling every day + { + let genDay; + let dayStart; + let dayEnd; + const sortieRolloverToday = getSortieTime(day); + if (Date.now() < sortieRolloverToday) { + // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`. + genDay = day - 1; + dayStart = getSortieTime(genDay); + dayEnd = sortieRolloverToday; + } else { + // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`. + genDay = day; + dayStart = sortieRolloverToday; + dayEnd = getSortieTime(day + 1); + } + + const rng = new CRng(genDay); + + const boss = rng.randomElement(sortieBosses); + + const modifiers = [ + "SORTIE_MODIFIER_LOW_ENERGY", + "SORTIE_MODIFIER_IMPACT", + "SORTIE_MODIFIER_SLASH", + "SORTIE_MODIFIER_PUNCTURE", + "SORTIE_MODIFIER_EXIMUS", + "SORTIE_MODIFIER_MAGNETIC", + "SORTIE_MODIFIER_CORROSIVE", + "SORTIE_MODIFIER_VIRAL", + "SORTIE_MODIFIER_ELECTRICITY", + "SORTIE_MODIFIER_RADIATION", + "SORTIE_MODIFIER_GAS", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_TOXIN", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_HAZARD_RADIATION", + "SORTIE_MODIFIER_HAZARD_MAGNETIC", + "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest + "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon + "SORTIE_MODIFIER_HAZARD_ICE", + "SORTIE_MODIFIER_HAZARD_COLD", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_MELEE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + + if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); + if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); + + const nodes: string[] = []; + const availableMissionIndexes: number[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && + sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 5 && // Exclude MT_CAPTURE + value.missionIndex != 21 && // Exclude MT_PURIFY + value.missionIndex != 23 && // Exclude MT_JUNCTION + value.missionIndex <= 28 + ) { + if (!availableMissionIndexes.includes(value.missionIndex)) { + availableMissionIndexes.push(value.missionIndex); + } + nodes.push(key); + } + } + + const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; + const missionTypes = new Set(); + + for (let i = 0; i < 3; i++) { + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; + let missionIndex = ExportRegions[node].missionIndex; + + if ( + !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions + missionIndex != 28 && + rng.randomInt(0, 2) == 2 + ) { + missionIndex = rng.randomElement(availableMissionIndexes); + } + + if (i == 2 && rng.randomInt(0, 2) == 2) { + const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); + const modifierType = rng.randomElement(filteredModifiers); + + if (boss == "SORTIE_BOSS_PHORID") { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); + nodes.splice(randomIndex, 1); + continue; + } else if (sortieBossNode[boss]) { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); + continue; + } + } + + const missionType = eMissionType[missionIndex].tag; + + if (missionTypes.has(missionType)) { + i--; + continue; + } + + const filteredModifiers = + missionType === "MT_TERRITORY" + ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") + : modifiers; + + const modifierType = rng.randomElement(filteredModifiers); + + selectedNodes.push({ missionType, modifierType, node }); + nodes.splice(randomIndex, 1); + missionTypes.add(missionType); + } + + worldState.Sorties.push({ + _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", + Seed: genDay, + Boss: boss, + Variants: selectedNodes + }); + } + + // Archon Hunt cycling every week + { + const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; + const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; + const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth + + const nodes: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.factionIndex !== undefined && + value.factionIndex < 2 && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 // Exclude MT_ASSASSINATION + ) { + nodes.push(key); + } + } + + const rng = new CRng(week); + const firstNodeIndex = rng.randomInt(0, nodes.length - 1); + const firstNode = nodes[firstNodeIndex]; + nodes.splice(firstNodeIndex, 1); + worldState.LiteSorties.push({ + _id: { + $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", + Seed: week, + Boss: boss, + Missions: [ + { + missionType: rng.randomElement([ + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_EXTERMINATION", + "MT_SABOTAGE", + "MT_RESCUE" + ]), + node: firstNode + }, + { + missionType: rng.randomElement([ + "MT_DEFENSE", + "MT_TERRITORY", + "MT_ARTIFACT", + "MT_EXCAVATE", + "MT_SURVIVAL" + ]), + node: rng.randomElement(nodes) + }, + { + missionType: "MT_ASSASSINATION", + node: showdownNode + } + ] + }); + } + + // Circuit choices cycling every week + worldState.EndlessXpChoices.push({ + Category: "EXC_NORMAL", + Choices: [ + ["Nidus", "Octavia", "Harrow"], + ["Gara", "Khora", "Revenant"], + ["Garuda", "Baruuk", "Hildryn"], + ["Excalibur", "Trinity", "Ember"], + ["Loki", "Mag", "Rhino"], + ["Ash", "Frost", "Nyx"], + ["Saryn", "Vauban", "Nova"], + ["Nekros", "Valkyr", "Oberon"], + ["Hydroid", "Mirage", "Limbo"], + ["Mesa", "Chroma", "Atlas"], + ["Ivara", "Inaros", "Titania"] + ][week % 12] + }); + worldState.EndlessXpChoices.push({ + Category: "EXC_HARD", + Choices: [ + ["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"], + ["Bo", "Latron", "Furis", "Furax", "Strun"], + ["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"], + ["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"], + ["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"], + ["Zylok", "Sibear", "Dread", "Despair", "Hate"], + ["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"], + ["Braton", "Lato", "Skana", "Paris", "Kunai"] + ][week % 8] + }); + + // 1999 Calendar Season cycling every week + YearIteration every 4 weeks + worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } }; + worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } }; + worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4]; + worldState.KnownCalendarSeasons[0].Days = [ + static1999WinterDays, + static1999SpringDays, + static1999SummerDays, + static1999FallDays + ][week % 4]; + worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4); + + // Sentient Anomaly cycling every 30 minutes + const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); + const tmp = { + cavabegin: "1690761600", + PurchasePlatformLockEnabled: true, + tcsn: true, + pgr: { + ts: "1732572900", + en: "CUSTOM DECALS @ ZEVILA", + fr: "DECALS CUSTOM @ ZEVILA", + it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA", + de: "AUFKLEBER NACH WUNSCH @ ZEVILA", + es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA", + pt: "DECALQUES PERSONALIZADOS NA ZEVILA", + ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа", + pl: "NOWE NAKLEJKI @ ZEVILA", + uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА", + tr: "ÖZEL ÇIKARTMALAR @ ZEVILA", + ja: "カスタムデカール @ ゼビラ", + zh: "定制贴花认准泽威拉", + ko: "커스텀 데칼 @ ZEVILA", + tc: "自訂貼花 @ ZEVILA", + th: "รูปลอกสั่งทำที่ ZEVILA" + }, + ennnd: true, + mbrt: true, + sfn: [550, 553, 554, 555][halfHour % 4] + }; + worldState.Tmp = JSON.stringify(tmp); + + return worldState; +}; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts new file mode 100644 index 00000000..a90f2134 --- /dev/null +++ b/src/types/worldStateTypes.ts @@ -0,0 +1,122 @@ +import { IMongoDate, IOid } from "./commonTypes"; + +export interface IWorldState { + Version: number; // for goals + BuildLabel: string; + Time: number; + Goals: IGoal[]; + SyndicateMissions: ISyndicateMission[]; + GlobalUpgrades: IGlobalUpgrade[]; + Sorties: ISortie[]; + LiteSorties: ILiteSortie[]; + NodeOverrides: INodeOverride[]; + EndlessXpChoices: IEndlessXpChoice[]; + SeasonInfo: { + Activation: IMongoDate; + Expiry: IMongoDate; + AffiliationTag: string; + Season: number; + Phase: number; + Params: string; + ActiveChallenges: ISeasonChallenge[]; + }; + KnownCalendarSeasons: ICalendarSeason[]; + Tmp?: string; +} + +export interface IGoal { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Count: number; + Goal: number; + Success: number; + Personal: boolean; + Desc: string; + ToolTip: string; + Icon: string; + Tag: string; + Node: string; +} + +export interface ISyndicateMission { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Tag: string; + Seed: number; + Nodes: string[]; +} + +export interface IGlobalUpgrade { + _id: IOid; + Activation: IMongoDate; + ExpiryDate: IMongoDate; + UpgradeType: string; + OperationType: string; + Value: number; + LocalizeTag: string; + LocalizeDescTag: string; +} + +export interface INodeOverride { + _id: IOid; + Activation?: IMongoDate; + Expiry?: IMongoDate; + Node: string; + Hide?: boolean; + Seed?: number; + LevelOverride?: string; + Faction?: string; + CustomNpcEncounters?: string; +} + +export interface ISortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards"; + Seed: number; + Boss: string; + Variants: { + missionType: string; + modifierType: string; + node: string; + }[]; +} + +export interface ILiteSortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; + Seed: number; + Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL" + Missions: { + missionType: string; + node: string; + }[]; +} + +export interface IEndlessXpChoice { + Category: string; + Choices: string[]; +} + +export interface ISeasonChallenge { + _id: IOid; + Daily?: boolean; + Activation: IMongoDate; + Expiry: IMongoDate; + Challenge: string; +} + +export interface ICalendarSeason { + Activation: IMongoDate; + Expiry: IMongoDate; + Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL" + Days: { + day: number; + }[]; + YearIteration: number; +} From 85b5bb438e49c052f519abd49ecfb2a0a1091698 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:53:50 -0700 Subject: [PATCH 433/776] feat: handle OtherDialogueInfos in saveDialogue (#1542) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1542 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 08df0676..999e0293 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,6 +1,7 @@ +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; @@ -22,31 +23,10 @@ export const saveDialogueController: RequestHandler = async (req, res) => { if (!inventory.DialogueHistory) { throw new Error("bad inventory state"); } - if (request.OtherDialogueInfos.length != 0 || !(request.Data || request.Gift)) { - logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); - } const inventoryChanges: IInventoryChanges = {}; const tomorrowAt0Utc = (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; inventory.DialogueHistory.Dialogues ??= []; - let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName); - if (!dialogue) { - dialogue = - inventory.DialogueHistory.Dialogues[ - inventory.DialogueHistory.Dialogues.push({ - Rank: 0, - Chemistry: 0, - AvailableDate: new Date(0), - AvailableGiftDate: new Date(0), - RankUpExpiry: new Date(0), - BountyChemExpiry: new Date(0), - QueuedDialogues: [], - Gifts: [], - Booleans: [], - Completed: [], - DialogueName: request.DialogueName - }) - 1 - ]; - } + const dialogue = getDialogue(inventory, request.DialogueName); dialogue.Rank = request.Rank; dialogue.Chemistry = request.Chemistry; if (request.Data) { @@ -69,6 +49,13 @@ export const saveDialogueController: RequestHandler = async (req, res) => { } dialogue.Completed.push(request.Data); dialogue.AvailableDate = new Date(tomorrowAt0Utc); + for (const info of request.OtherDialogueInfos) { + const otherDialogue = getDialogue(inventory, info.Dialogue); + if (info.Tag != "") { + otherDialogue.QueuedDialogues.push(info.Tag); + } + otherDialogue.Chemistry += info.Value; // unsure + } await inventory.save(); res.json({ InventoryChanges: inventoryChanges, @@ -88,6 +75,8 @@ export const saveDialogueController: RequestHandler = async (req, res) => { InventoryChanges: inventoryChanges, AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } }); + } else { + logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); } } }; @@ -113,7 +102,7 @@ interface SaveCompletedDialogueRequest { Booleans: string[]; ResetBooleans: string[]; Data?: ICompletedDialogue; - OtherDialogueInfos: IOtherDialogueInfo[]; // unsure + OtherDialogueInfos: IOtherDialogueInfo[]; } interface IOtherDialogueInfo { @@ -121,3 +110,26 @@ interface IOtherDialogueInfo { Tag: string; Value: number; } + +const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { + let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName); + if (!dialogue) { + dialogue = + inventory.DialogueHistory!.Dialogues![ + inventory.DialogueHistory!.Dialogues!.push({ + Rank: 0, + Chemistry: 0, + AvailableDate: new Date(0), + AvailableGiftDate: new Date(0), + RankUpExpiry: new Date(0), + BountyChemExpiry: new Date(0), + QueuedDialogues: [], + Gifts: [], + Booleans: [], + Completed: [], + DialogueName: dialogueName + }) - 1 + ]; + } + return dialogue; +}; From cc338c21730dc219c353cab01c14f2aeb4ef0a5c Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:54:07 -0700 Subject: [PATCH 434/776] feat: cheat Unlock All Dojo Deco Recipes (#1543) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1543 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/models/guildModel.ts | 1 + src/services/configService.ts | 1 + src/services/guildService.ts | 6 +- src/types/guildTypes.ts | 1 + static/fixed_responses/allDecoRecipes.json | 92 ++++++++++++++++++++++ static/webui/index.html | 4 + static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 11 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 static/fixed_responses/allDecoRecipes.json diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 7cd28c6c..ffb5dac5 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -191,6 +191,7 @@ const guildSchema = new Schema( VaultMiscItems: { type: [typeCountSchema], default: undefined }, VaultShipDecorations: { type: [typeCountSchema], default: undefined }, VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined }, + VaultDecoRecipes: { type: [typeCountSchema], default: undefined }, TechProjects: { type: [techProjectSchema], default: undefined }, ActiveDojoColorResearch: { type: String, default: "" }, Class: { type: Number, default: 0 }, diff --git a/src/services/configService.ts b/src/services/configService.ts index 86461644..7a3f0b1d 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -29,6 +29,7 @@ interface IConfig { unlockAllFlavourItems?: boolean; unlockAllSkins?: boolean; unlockAllCapturaScenes?: boolean; + unlockAllDecoRecipes?: boolean; universalPolarityEverywhere?: boolean; unlockDoubleCapacityPotatoesEverywhere?: boolean; unlockExilusEverywhere?: boolean; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 3dbddc02..15b973f8 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -30,6 +30,7 @@ import { Inbox } from "../models/inboxModel"; import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "../types/purchaseTypes"; import { parallelForeach } from "../utils/async-utils"; +import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -114,7 +115,10 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => { DojoRefundMiscItems: guild.VaultMiscItems, DojoRefundPremiumCredits: guild.VaultPremiumCredits, ShipDecorations: guild.VaultShipDecorations, - FusionTreasures: guild.VaultFusionTreasures + FusionTreasures: guild.VaultFusionTreasures, + DecoRecipes: config.unlockAllDecoRecipes + ? allDecoRecipes.map(recipe => ({ ItemType: recipe, ItemCount: 1 })) + : guild.VaultDecoRecipes }; }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 518e6bcf..0d02b0db 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -44,6 +44,7 @@ export interface IGuildDatabase { VaultMiscItems?: IMiscItem[]; VaultShipDecorations?: ITypeCount[]; VaultFusionTreasures?: IFusionTreasure[]; + VaultDecoRecipes?: ITypeCount[]; TechProjects?: ITechProjectDatabase[]; ActiveDojoColorResearch: string; diff --git a/static/fixed_responses/allDecoRecipes.json b/static/fixed_responses/allDecoRecipes.json new file mode 100644 index 00000000..4ed19d5b --- /dev/null +++ b/static/fixed_responses/allDecoRecipes.json @@ -0,0 +1,92 @@ +[ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyBronzeARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyGoldARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyPlatinumARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophySilverARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBaseTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EyesOfBlightTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FusionMoaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBaseTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventPewterTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyTerracottaRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinMusicBoxRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RailjackResearchTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SlingStoneTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SpyDroneTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoGhostTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoMoonTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoMountainTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoShadowTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoStormTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyCrystalRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CorpusPlaceables/GasTurbineConeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NaturalPlaceables/CoralChunkARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoPlaceables/TnoBeaconEmitterRecipe" +] diff --git a/static/webui/index.html b/static/webui/index.html index 7d3af19c..afd841cd 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -568,6 +568,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 6d20ca01..5f000ff4 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -127,6 +127,7 @@ dict = { cheats_unlockAllFlavourItems: `Alle Sammlerstücke freischalten`, cheats_unlockAllSkins: `Alle Skins freischalten`, cheats_unlockAllCapturaScenes: `Alle Photora-Szenen freischalten`, + cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`, cheats_universalPolarityEverywhere: `Universelle Polarität überall`, cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`, cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 12638068..700e8566 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -126,6 +126,7 @@ dict = { cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, cheats_unlockAllSkins: `Unlock All Skins`, cheats_unlockAllCapturaScenes: `Unlock All Captura Scenes`, + cheats_unlockAllDecoRecipes: `Unlock All Dojo Deco Recipes`, cheats_universalPolarityEverywhere: `Universal Polarity Everywhere`, cheats_unlockDoubleCapacityPotatoesEverywhere: `Potatoes Everywhere`, cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 6d619821..03807992 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -127,6 +127,7 @@ dict = { cheats_unlockAllFlavourItems: `Débloquer tous les Flavor Items`, cheats_unlockAllSkins: `Débloquer tous les skins`, cheats_unlockAllCapturaScenes: `Débloquer toutes les scènes captura`, + cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`, cheats_universalPolarityEverywhere: `Polarités universelles partout`, cheats_unlockDoubleCapacityPotatoesEverywhere: `Réacteurs et Catalyseurs partout`, cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 62ff74c7..2299f2c1 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -127,6 +127,7 @@ dict = { cheats_unlockAllFlavourItems: `Разблокировать все уникальные предметы`, cheats_unlockAllSkins: `Разблокировать все скины`, cheats_unlockAllCapturaScenes: `Разблокировать все сцены Каптуры`, + cheats_unlockAllDecoRecipes: `Разблокировать все рецепты декораций Дoдзё`, cheats_universalPolarityEverywhere: `Универсальная полярность везде`, cheats_unlockDoubleCapacityPotatoesEverywhere: `Катализаторы везде`, cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 246d6dca..7598b005 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -127,6 +127,7 @@ dict = { cheats_unlockAllFlavourItems: `解锁所有装饰物品`, cheats_unlockAllSkins: `解锁所有外观`, cheats_unlockAllCapturaScenes: `解锁所有Captura场景`, + cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`, cheats_universalPolarityEverywhere: `全局万用极性`, cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`, cheats_unlockExilusEverywhere: `全物品自带适配器`, From ec8982a92171eff15ca901b86c4611e57cf8dcea Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:54:35 -0700 Subject: [PATCH 435/776] feat: bounty rewards (#1549) Re #388. This only handles reward manifests and only those given in the worldState. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1549 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/missionInventoryUpdateController.ts | 3 ++- src/services/missionInventoryUpdateService.ts | 18 ++++++++++++++++-- src/types/requestTypes.ts | 6 +++--- src/types/worldStateTypes.ts | 15 +++++++++++++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 4dc5bd66..1a8e6318 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -55,7 +55,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) const inventory = await getInventory(accountId); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); - if (missionReport.MissionStatus !== "GS_SUCCESS") { + // skip mission rewards if not GS_SUCCESS and not a bounty (by presence of jobId, as there's a reward every stage but only the last stage has GS_SUCCESS) + if (missionReport.MissionStatus !== "GS_SUCCESS" && !missionReport.RewardInfo?.jobId) { await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); res.json({ diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 9ca788af..376656c6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -50,6 +50,7 @@ import conservationAnimals from "@/static/fixed_responses/conservationAnimals.js import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; +import { getWorldState } from "./worldStateService"; const getRotations = (rotationCount: number, tierOverride: number | undefined): number[] => { if (rotationCount === 0) return [0]; @@ -806,13 +807,23 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u const drops: IMissionReward[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; - const rewardManifests: string[] = + let rewardManifests: string[] = RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB" ? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"] : region.rewardManifests; let rotations: number[] = []; - if (RewardInfo.VaultsCracked) { + if (RewardInfo.jobId) { + if (RewardInfo.JobTier! >= 0) { + const id = RewardInfo.jobId.split("_")[3]; + const syndicateInfo = getWorldState().SyndicateMissions.find(x => x._id.$oid == id); + if (syndicateInfo) { + const jobInfo = syndicateInfo.Jobs![RewardInfo.JobTier!]; + rewardManifests = [jobInfo.rewards]; + rotations = [RewardInfo.JobStage!]; + } + } + } else if (RewardInfo.VaultsCracked) { // For Spy missions, e.g. 3 vaults cracked = A, B, C for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { rotations.push(i); @@ -821,6 +832,9 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u const rotationCount = RewardInfo.rewardQualifications?.length || 0; rotations = getRotations(rotationCount, tierOverride); } + if (rewardManifests.length != 0) { + logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); + } rewardManifests .map(name => ExportRewards[name]) .forEach(table => { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 1f00fa45..6e732f83 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -145,10 +145,10 @@ export interface IRewardInfo { periodicMissionTag?: string; // for bounties, only EOM_AFK and node are given from above, plus: - JobTier?: string; + JobTier?: number; jobId?: string; - JobStage?: string; - Q?: boolean; // always false? + JobStage?: number; + Q?: boolean; // likely indicates that the bonus objective for this stage was completed CheckpointCounter?: number; // starts at 1, is incremented with each job stage upload, and does not reset when starting a new job } diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index a90f2134..a3b6efac 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -5,7 +5,7 @@ export interface IWorldState { BuildLabel: string; Time: number; Goals: IGoal[]; - SyndicateMissions: ISyndicateMission[]; + SyndicateMissions: ISyndicateMissionInfo[]; GlobalUpgrades: IGlobalUpgrade[]; Sorties: ISortie[]; LiteSorties: ILiteSortie[]; @@ -39,13 +39,24 @@ export interface IGoal { Node: string; } -export interface ISyndicateMission { +export interface ISyndicateMissionInfo { _id: IOid; Activation: IMongoDate; Expiry: IMongoDate; Tag: string; Seed: number; Nodes: string[]; + Jobs?: { + jobType?: string; + rewards: string; + masteryReq: number; + minEnemyLevel: number; + maxEnemyLevel: number; + xpAmounts: number[]; + endless?: boolean; + locationTag?: string; + isVault?: boolean; + }[]; } export interface IGlobalUpgrade { From 5149d0e3820ac5983a782dcfaf70129d430f2611 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:54:47 -0700 Subject: [PATCH 436/776] chore: don't use ? for leaderboardService parameters (#1551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This means the argument can be omitted when we really mean that it can be undefined — but we still want it to be explicitly given. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1551 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/leaderboardService.ts | 8 ++++---- src/services/statsService.ts | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts index b0e03518..2ef908f7 100644 --- a/src/services/leaderboardService.ts +++ b/src/services/leaderboardService.ts @@ -8,7 +8,7 @@ export const submitLeaderboardScore = async ( ownerId: string, displayName: string, score: number, - guildId?: string + guildId: string | undefined ): Promise => { let expiry: Date; if (schedule == "daily") { @@ -39,9 +39,9 @@ export const getLeaderboard = async ( leaderboard: string, before: number, after: number, - pivotId?: string, - guildId?: string, - guildTier?: number + pivotId: string | undefined, + guildId: string | undefined, + guildTier: number | undefined ): Promise => { const filter: { leaderboard: string; guildId?: string; guildTier?: number } = { leaderboard }; if (guildId) { diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 601aae7b..626bbe27 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -378,7 +378,8 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) category, accountOwnerId, payload.displayName, - data as number + data as number, + payload.guildId ); break; From f0351489be8ad72aeac8cc6020034a6f6374bbb4 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:54:59 -0700 Subject: [PATCH 437/776] feat: HeistProfitTakerBountyThree first time completion reward (#1552) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1552 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 376656c6..b52b09d7 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -166,6 +166,19 @@ export const addMissionInventoryUpdates = async ( } if (!chain.Jobs.includes(bounty)) { chain.Jobs.push(bounty); + if (bounty == "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree") { + await createMessage(inventory.accountOwnerId, [ + { + sub: "/Lotus/Language/SolarisHeists/HeavyCatalystInboxTitle", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/SolarisHeists/HeavyCatalystInboxMessage", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Types/Restoratives/HeavyWeaponSummon"], + highPriority: true + } + ]); + await addItem(inventory, "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst", 1); + } } } } From 63e3c966718ccad61b6c7ad3926b8ed2e9d33bc3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:56:12 -0700 Subject: [PATCH 438/776] feat: transmutation of requiem/antivirus/potency mods (#1553) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1553 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/artifactTransmutationController.ts | 86 ++++++++++++++----- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts index 78da93a1..7e39e79e 100644 --- a/src/controllers/api/artifactTransmutationController.ts +++ b/src/controllers/api/artifactTransmutationController.ts @@ -57,12 +57,16 @@ export const artifactTransmutationController: RequestHandler = async (req, res) payload.Consumed.forEach(upgrade => { const meta = ExportUpgrades[upgrade.ItemType]; counts[meta.rarity] += upgrade.ItemCount; - addMods(inventory, [ - { - ItemType: upgrade.ItemType, - ItemCount: upgrade.ItemCount * -1 - } - ]); + if (upgrade.ItemId.$oid != "000000000000000000000000") { + inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid }); + } else { + addMods(inventory, [ + { + ItemType: upgrade.ItemType, + ItemCount: upgrade.ItemCount * -1 + } + ]); + } if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") { forcedPolarity = "AP_ATTACK"; } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") { @@ -72,22 +76,33 @@ export const artifactTransmutationController: RequestHandler = async (req, res) } }); - // Based on the table on https://wiki.warframe.com/w/Transmutation - const weights: Record = { - COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4, - UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10, - RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50, - LEGENDARY: 0 - }; - - const options: { uniqueName: string; rarity: TRarity }[] = []; - Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => { - if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) { - options.push({ uniqueName, rarity: upgrade.rarity }); + let newModType: string | undefined; + for (const specialModSet of specialModSets) { + if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) { + newModType = getRandomElement(specialModSet); + break; } - }); + } + + if (!newModType) { + // Based on the table on https://wiki.warframe.com/w/Transmutation + const weights: Record = { + COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4, + UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10, + RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50, + LEGENDARY: 0 + }; + + const options: { uniqueName: string; rarity: TRarity }[] = []; + Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => { + if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) { + options.push({ uniqueName, rarity: upgrade.rarity }); + } + }); + + newModType = getRandomWeightedReward(options, weights)!.uniqueName; + } - const newModType = getRandomWeightedReward(options, weights)!.uniqueName; addMods(inventory, [ { ItemType: newModType, @@ -130,3 +145,34 @@ interface IAgnosticUpgradeClient { ItemCount: number; LastAdded: IOid; } + +const specialModSets: string[][] = [ + [ + "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod" + ], + [ + "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" + ], + [ + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod" + ] +]; From 2ca79ef8986f904978408b3601fd517651d373de Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:56:31 -0700 Subject: [PATCH 439/776] feat: eidolon/venus/deimos bounty rotation (#1554) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1554 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 104 ++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 11259586..f5dde681 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -9,6 +9,7 @@ import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; +import { logger } from "../utils/logger"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -79,6 +80,75 @@ const sortieBossNode: Record = { SORTIE_BOSS_INFALAD: "SolNode705" }; +const jobSets: string[][] = [ + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", + "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc" + ], + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" + ], + [ + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy" + ], + [ + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation" + ], + [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty" + ], + [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" + ] +]; + const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 const getSortieTime = (day: number): number => { @@ -249,6 +319,40 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Seed: bountyCycle, Nodes: [] }; + for (const syndicateInfo of worldState.SyndicateMissions) { + if (syndicateInfo.Jobs && syndicateInfo.Seed != bountyCycle) { + syndicateInfo.Seed = bountyCycle; + logger.debug(`refreshing jobs for ${syndicateInfo.Tag}`); + const rng = new CRng(bountyCycle); + const table = String.fromCharCode(65 + (bountyCycle % 3)); + const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); + const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); + //console.log({ bountyCycleStart, bountyCycleEnd, table, vaultTable, deimosDTable }); + for (const jobInfo of syndicateInfo.Jobs) { + if (jobInfo.jobType) { + let found = false; + for (const jobSet of jobSets) { + if (jobSet.indexOf(jobInfo.jobType) != -1) { + jobInfo.jobType = rng.randomElement(jobSet); + // TODO: xpAmounts seems like it might need to differ depending on the job? + found = true; + break; + } + } + if (!found) { + logger.warn(`no job set found for type ${jobInfo.jobType}`); + } + } + if (jobInfo.endless || jobInfo.isVault) { + jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${vaultTable}Rewards`); + } else if (jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierD")) { + jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${deimosDTable}Rewards`); + } else if (!jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierE")) { + jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${table}Rewards`); + } + } + } + } if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ From dde95c2b6165d07d5d17c47b6e3c3cca0186d72e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:56:45 -0700 Subject: [PATCH 440/776] feat: favoriting equipment & skins (#1555) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1555 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveLoadout.ts | 32 ------------------- src/controllers/api/saveLoadoutController.ts | 21 ++++++++++++ src/models/inventoryModels/inventoryModel.ts | 2 ++ src/routes/api.ts | 2 +- src/services/saveLoadoutService.ts | 20 +++++++++++- .../inventoryTypes/commonInventoryTypes.ts | 1 + src/types/inventoryTypes/inventoryTypes.ts | 1 + src/types/saveLoadoutTypes.ts | 2 +- 8 files changed, 46 insertions(+), 35 deletions(-) delete mode 100644 src/controllers/api/saveLoadout.ts create mode 100644 src/controllers/api/saveLoadoutController.ts diff --git a/src/controllers/api/saveLoadout.ts b/src/controllers/api/saveLoadout.ts deleted file mode 100644 index 38ea5559..00000000 --- a/src/controllers/api/saveLoadout.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { RequestHandler } from "express"; -import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; -import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { logger } from "@/src/utils/logger"; - -export const saveLoadoutController: RequestHandler = async (req, res) => { - //validate here - const accountId = await getAccountIdForRequest(req); - - try { - const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest; - // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { UpgradeVer, ...equipmentChanges } = body; - const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId); - - //send back new loadout id, if new loadout was added - if (newLoadoutId) { - res.send(newLoadoutId); - } - res.status(200).end(); - } catch (error: unknown) { - if (error instanceof Error) { - logger.error(`error in saveLoadoutController: ${error.message}`); - res.status(400).json({ error: error.message }); - } else { - res.status(400).json({ error: "unknown error" }); - } - } -}; diff --git a/src/controllers/api/saveLoadoutController.ts b/src/controllers/api/saveLoadoutController.ts new file mode 100644 index 00000000..6fd3de61 --- /dev/null +++ b/src/controllers/api/saveLoadoutController.ts @@ -0,0 +1,21 @@ +import { RequestHandler } from "express"; +import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; +import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; + +export const saveLoadoutController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + + const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest; + // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { UpgradeVer, ...equipmentChanges } = body; + const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId); + + //send back new loadout id, if new loadout was added + if (newLoadoutId) { + res.send(newLoadoutId); + } + res.end(); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 01b5aca5..0cb698cc 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -612,6 +612,7 @@ const spectreLoadoutsSchema = new Schema( const weaponSkinsSchema = new Schema( { ItemType: String, + Favorite: Boolean, IsNew: Boolean }, { id: false } @@ -880,6 +881,7 @@ const EquipmentSchema = new Schema( RailjackImage: FlavourItemSchema, CrewMembers: crewShipMembersSchema, Details: detailsSchema, + Favorite: Boolean, IsNew: Boolean }, { id: false } diff --git a/src/routes/api.ts b/src/routes/api.ts index 7dfd562f..385553d4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -97,7 +97,7 @@ import { removeFromGuildController } from "@/src/controllers/api/removeFromGuild import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; -import { saveLoadoutController } from "@/src/controllers/api/saveLoadout"; +import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController"; import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController"; import { sellController } from "@/src/controllers/api/sellController"; diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 2f8711c6..472c599d 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -140,7 +140,22 @@ export const handleInventoryItemConfigChange = async ( case "WeaponSkins": { const itemEntries = equipment as IItemEntry; for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) { - inventory.WeaponSkins.id(itemId)!.IsNew = itemConfigEntries.IsNew; + if (itemId.startsWith("ca70ca70ca70ca70")) { + logger.warn( + `unlockAllSkins does not work with favoriting items because you don't actually own it` + ); + } else { + const inventoryItem = inventory.WeaponSkins.id(itemId); + if (!inventoryItem) { + throw new Error(`inventory item WeaponSkins not found with id ${itemId}`); + } + if ("Favorite" in itemConfigEntries) { + inventoryItem.Favorite = itemConfigEntries.Favorite; + } + if ("IsNew" in itemConfigEntries) { + inventoryItem.IsNew = itemConfigEntries.IsNew; + } + } } break; } @@ -163,6 +178,9 @@ export const handleInventoryItemConfigChange = async ( inventoryItem.Configs[parseInt(configId)] = config; } } + if ("Favorite" in itemConfigEntries) { + inventoryItem.Favorite = itemConfigEntries.Favorite; + } if ("IsNew" in itemConfigEntries) { inventoryItem.IsNew = itemConfigEntries.IsNew; } diff --git a/src/types/inventoryTypes/commonInventoryTypes.ts b/src/types/inventoryTypes/commonInventoryTypes.ts index 06a8ec9c..37168a7d 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -142,6 +142,7 @@ export interface IEquipmentDatabase { RailjackImage?: IFlavourItem; CrewMembers?: ICrewShipMembersDatabase; Details?: IKubrowPetDetailsDatabase; + Favorite?: boolean; IsNew?: boolean; _id: Types.ObjectId; } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index c092fb9f..c65b426c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1034,6 +1034,7 @@ export interface ITaunt { export interface IWeaponSkinDatabase { ItemType: string; + Favorite?: boolean; IsNew?: boolean; _id: Types.ObjectId; } diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index b496a4ea..61f1aba4 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -51,7 +51,7 @@ export interface IItemEntry { export type IConfigEntry = { [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; -} & { IsNew?: boolean }; +} & { Favorite?: boolean; IsNew?: boolean }; export type ILoadoutClient = Omit; From 70fa48ab071bcf132284a0d27c07af2847ac138d Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Fri, 11 Apr 2025 06:57:35 -0700 Subject: [PATCH 441/776] chore(webui): update to German translation (#1568) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1568 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 5f000ff4..265df59e 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -138,7 +138,7 @@ dict = { cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, - cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, + cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, cheats_noDojoResearchCosts: `Keine Dojo-Forschungskosten`, cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`, From 61e168e4441b3b1fbb687ae4193ac142131751a0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:55:23 +0200 Subject: [PATCH 442/776] chore: add Conselor to IInventoryClient this isn't an accolade, but it unlocks a new chat --- src/types/inventoryTypes/inventoryTypes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index c65b426c..63d1b315 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -238,6 +238,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Accolades?: { Heirloom?: boolean; }; + Counselor?: boolean; Upgrades: IUpgradeClient[]; EquippedGear: string[]; DeathMarks: string[]; From 355de3fa0411addb055406137e75abb7fb07b620 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Fri, 11 Apr 2025 08:51:39 -0700 Subject: [PATCH 443/776] chore(webui): update to German translation (#1575) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1575 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 265df59e..ea662e37 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -127,7 +127,7 @@ dict = { cheats_unlockAllFlavourItems: `Alle Sammlerstücke freischalten`, cheats_unlockAllSkins: `Alle Skins freischalten`, cheats_unlockAllCapturaScenes: `Alle Photora-Szenen freischalten`, - cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`, + cheats_unlockAllDecoRecipes: `Alle Dojo-Deko-Baupläne freischalten`, cheats_universalPolarityEverywhere: `Universelle Polarität überall`, cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`, cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, From 946f3129b8189653dbc704dc23c8479918906479 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:13:44 -0700 Subject: [PATCH 444/776] feat: bounty standing reward (#1556) Re #388 I think this only missing `Field Bounties` and `Arcana Isolation Vault` Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1556 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/fishmongerController.ts | 28 +----- .../api/missionInventoryUpdateController.ts | 13 ++- .../api/syndicateStandingBonusController.ts | 38 +------ src/services/inventoryService.ts | 40 ++++++-- src/services/missionInventoryUpdateService.ts | 99 ++++++++++++++++++- src/types/requestTypes.ts | 1 + 6 files changed, 149 insertions(+), 70 deletions(-) diff --git a/src/controllers/api/fishmongerController.ts b/src/controllers/api/fishmongerController.ts index 898f5e4c..d85f7a4c 100644 --- a/src/controllers/api/fishmongerController.ts +++ b/src/controllers/api/fishmongerController.ts @@ -1,10 +1,9 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; -import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService"; +import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; -import { ExportResources, ExportSyndicates } from "warframe-public-export-plus"; +import { ExportResources } from "warframe-public-export-plus"; export const fishmongerController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -31,32 +30,15 @@ export const fishmongerController: RequestHandler = async (req, res) => { miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 }); } addMiscItems(inventory, miscItemChanges); - if (gainedStanding && syndicateTag) { - let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); - if (!syndicate) { - syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1]; - } - const syndicateMeta = ExportSyndicates[syndicateTag]; - - const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); - if (syndicate.Standing + gainedStanding > max) { - gainedStanding = max - syndicate.Standing; - } - if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { - gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); - } - - syndicate.Standing += gainedStanding; - - updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding); - } + let affiliationMod; + if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding); await inventory.save(); res.json({ InventoryChanges: { MiscItems: miscItemChanges }, SyndicateTag: syndicateTag, - StandingChange: gainedStanding + StandingChange: affiliationMod?.Standing || 0 }); }; diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 1a8e6318..738002fd 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -55,8 +55,10 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) const inventory = await getInventory(accountId); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); - // skip mission rewards if not GS_SUCCESS and not a bounty (by presence of jobId, as there's a reward every stage but only the last stage has GS_SUCCESS) - if (missionReport.MissionStatus !== "GS_SUCCESS" && !missionReport.RewardInfo?.jobId) { + if ( + missionReport.MissionStatus !== "GS_SUCCESS" && + !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId) + ) { await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); res.json({ @@ -66,7 +68,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) return; } - const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport); + const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } = + await addMissionRewards(inventory, missionReport); await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); @@ -78,7 +81,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) MissionRewards, ...credits, ...inventoryUpdates, - FusionPoints: inventoryChanges?.FusionPoints + FusionPoints: inventoryChanges?.FusionPoints, + SyndicateXPItemReward, + AffiliationMods }); }; diff --git a/src/controllers/api/syndicateStandingBonusController.ts b/src/controllers/api/syndicateStandingBonusController.ts index 4067e9db..3170bc93 100644 --- a/src/controllers/api/syndicateStandingBonusController.ts +++ b/src/controllers/api/syndicateStandingBonusController.ts @@ -1,16 +1,9 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { - addMiscItems, - freeUpSlot, - getInventory, - getStandingLimit, - updateStandingLimit -} from "@/src/services/inventoryService"; +import { addMiscItems, addStanding, freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IOid } from "@/src/types/commonTypes"; import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; -import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { logger } from "@/src/utils/logger"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; @@ -61,38 +54,13 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; } - let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag); - if (!syndicate) { - syndicate = - inventory.Affiliations[ - inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1 - ]; - } - - const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); - if (syndicate.Standing + gainedStanding > max) { - gainedStanding = max - syndicate.Standing; - } - - if (syndicateMeta.medallionsCappedByDailyLimit) { - if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { - gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); - } - updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding); - } - - syndicate.Standing += gainedStanding; + const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true); await inventory.save(); res.json({ InventoryChanges: inventoryChanges, - AffiliationMods: [ - { - Tag: request.Operation.AffiliationTag, - Standing: gainedStanding - } - ] + AffiliationMods: [affiliationMod] }); }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 078e7cd2..e29ea0a5 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -65,6 +65,7 @@ import { handleBundleAcqusition } from "./purchaseService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { getRandomElement, getRandomInt, SRng } from "./rngService"; import { createMessage } from "./inboxService"; +import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -930,23 +931,50 @@ const standingLimitBinToInventoryKey: Record< export const allDailyAffiliationKeys: (keyof IDailyAffiliations)[] = Object.values(standingLimitBinToInventoryKey); -export const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => { +const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => { if (bin == "STANDING_LIMIT_BIN_NONE" || config.noDailyStandingLimits) { return Number.MAX_SAFE_INTEGER; } return inventory[standingLimitBinToInventoryKey[bin]]; }; -export const updateStandingLimit = ( - inventory: IDailyAffiliations, - bin: TStandingLimitBin, - subtrahend: number -): void => { +const updateStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin, subtrahend: number): void => { if (bin != "STANDING_LIMIT_BIN_NONE" && !config.noDailyStandingLimits) { inventory[standingLimitBinToInventoryKey[bin]] -= subtrahend; } }; +export const addStanding = ( + inventory: TInventoryDatabaseDocument, + syndicateTag: string, + gainedStanding: number, + isMedallion: boolean = false +): IAffiliationMods => { + let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); + const syndicateMeta = ExportSyndicates[syndicateTag]; + + if (!syndicate) { + syndicate = + inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0, Title: 0 }) - 1]; + } + + const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); + if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing; + + if (!isMedallion || (isMedallion && syndicateMeta.medallionsCappedByDailyLimit)) { + if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { + gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); + } + updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding); + } + + syndicate.Standing += gainedStanding; + return { + Tag: syndicateTag, + Standing: gainedStanding + }; +}; + // TODO: AffiliationMods support (Nightwave). export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems"); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b52b09d7..e6492a6e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -28,13 +28,14 @@ import { addMods, addRecipes, addShipDecorations, + addStanding, combineInventoryChanges, updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; import { Types } from "mongoose"; -import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes"; import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; @@ -529,6 +530,8 @@ interface AddMissionRewardsReturnType { MissionRewards: IMissionReward[]; inventoryChanges?: IInventoryChanges; credits?: IMissionCredits; + AffiliationMods?: IAffiliationMods[]; + SyndicateXPItemReward?: number; } //TODO: return type of partial missioninventoryupdate response @@ -555,6 +558,8 @@ export const addMissionRewards = async ( const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; + const AffiliationMods: IAffiliationMods[] = []; + let SyndicateXPItemReward; let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display @@ -718,7 +723,97 @@ export const addMissionRewards = async ( inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; } } - return { inventoryChanges, MissionRewards, credits }; + + if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, tierStr, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_"); + const tier = Number(tierStr); + + const worldState = getWorldState(); + let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId); + if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag + if (syndicateEntry && syndicateEntry.Jobs) { + let currentJob = syndicateEntry.Jobs[tier]; + if (syndicateEntry.Tag === "EntratiSyndicate") { + const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); + if (vault) currentJob = vault; + let medallionAmount = currentJob.xpAmounts[rewardInfo.JobStage]; + + if ( + ["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some( + ending => jobType.endsWith(ending) + ) + ) { + const endlessJob = syndicateEntry.Jobs.find(j => j.endless); + if (endlessJob) { + const index = rewardInfo.JobStage % endlessJob.xpAmounts.length; + const excess = Math.floor(rewardInfo.JobStage / endlessJob.xpAmounts.length); + medallionAmount = Math.floor(endlessJob.xpAmounts[index] * (1 + 0.15000001 * excess)); + } + } + await addItem(inventory, "/Lotus/Types/Items/Deimos/EntratiFragmentUncommonB", medallionAmount); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/Deimos/EntratiFragmentUncommonB", + ItemCount: medallionAmount + }); + SyndicateXPItemReward = medallionAmount; + } else { + if (tier >= 0) { + AffiliationMods.push( + addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage]) + ); + } else { + if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) { + AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000)); + } + if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) { + AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 5000)); + } + if ( + [ + "Hunts/TeralystHunt", + "Heists/HeistProfitTakerBountyTwo", + "Heists/HeistProfitTakerBountyThree", + "Heists/HeistProfitTakerBountyFour", + "Heists/HeistExploiterBountyOne" + ].some(ending => jobType.endsWith(ending)) + ) { + AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000)); + } + } + } + } + } + + if (rewardInfo.challengeMissionId) { + const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points + const tier = Number(tierStr); + const isSteelPath = missions?.Tier; + if (syndicateTag === "ZarimanSyndicate") { + let medallionAmount = tier + 1; + if (isSteelPath) medallionAmount = Math.round(medallionAmount * 1.5); + await addItem(inventory, "/Lotus/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty", medallionAmount); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty", + ItemCount: medallionAmount + }); + SyndicateXPItemReward = medallionAmount; + } else { + let standingAmount = (tier + 1) * 1000; + if (tier > 5) standingAmount = 7500; // InfestedLichBounty + if (isSteelPath) standingAmount *= 1.5; + AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount)); + } + if (isSteelPath) { + await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", + ItemCount: 1 + }); + } + } + + return { inventoryChanges, MissionRewards, credits, AffiliationMods, SyndicateXPItemReward }; }; interface IMissionCredits { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 6e732f83..494e23f4 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -150,6 +150,7 @@ export interface IRewardInfo { JobStage?: number; Q?: boolean; // likely indicates that the bonus objective for this stage was completed CheckpointCounter?: number; // starts at 1, is incremented with each job stage upload, and does not reset when starting a new job + challengeMissionId?: string; } export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; From 0c1fa05e9c16b8452bb488b464f8956b734735ff Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:14:02 -0700 Subject: [PATCH 445/776] chore: don't error on setDojoURL (#1571) users may be confused about the "unknown endpoint" message, as it is reported with error level Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1571 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/dojoController.ts | 6 ++++++ src/routes/api.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/dojoController.ts b/src/controllers/api/dojoController.ts index c14d9316..9c54b449 100644 --- a/src/controllers/api/dojoController.ts +++ b/src/controllers/api/dojoController.ts @@ -1,5 +1,11 @@ import { RequestHandler } from "express"; +// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL. + export const dojoController: RequestHandler = (_req, res) => { res.json("-1"); // Tell client to use authorised request. }; + +export const setDojoURLController: RequestHandler = (_req, res) => { + res.end(); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 385553d4..77b389a3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -35,7 +35,7 @@ import { deleteSessionController } from "@/src/controllers/api/deleteSessionCont import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; import { divvyAllianceVaultController } from "@/src/controllers/api/divvyAllianceVaultController"; import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; -import { dojoController } from "@/src/controllers/api/dojoController"; +import { dojoController, setDojoURLController } from "@/src/controllers/api/dojoController"; import { dronesController } from "@/src/controllers/api/dronesController"; import { endlessXpController } from "@/src/controllers/api/endlessXpController"; import { entratiLabConquestModeController } from "@/src/controllers/api/entratiLabConquestModeController"; @@ -183,6 +183,7 @@ apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController); apiRouter.get("/setBootLocation.php", setBootLocationController); +apiRouter.get("/setDojoURL", setDojoURLController); apiRouter.get("/setGuildMotd.php", setGuildMotdController); apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController); apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController); From 525e3067c95b965d220fa9b62ac0c0100f3230d9 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:14:21 -0700 Subject: [PATCH 446/776] fix: only warn when addKeyChainItems does not change the inventory (#1578) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1578 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 bae74f66..85cefa4b 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -232,7 +232,7 @@ export const giveKeyChainItem = async ( const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); if (isEmptyObject(inventoryChanges)) { - throw new Error("inventory changes was empty after getting keychain items: should not happen"); + logger.warn("inventory changes was empty after getting keychain items: should not happen"); } // items were added: update quest stage's i (item was given) updateQuestStage(inventory, keyChainInfo, { i: true }); From 97d27e8110bad883156571a284df3c82f5048b64 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:14:46 -0700 Subject: [PATCH 447/776] feat: playedParkourTutorial (#1579) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1579 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/playedParkourTutorialController.ts | 9 +++++++++ src/routes/api.ts | 2 ++ src/services/inventoryService.ts | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/playedParkourTutorialController.ts diff --git a/src/controllers/api/playedParkourTutorialController.ts b/src/controllers/api/playedParkourTutorialController.ts new file mode 100644 index 00000000..a37689f2 --- /dev/null +++ b/src/controllers/api/playedParkourTutorialController.ts @@ -0,0 +1,9 @@ +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const playedParkourTutorialController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true }); + res.end(); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 77b389a3..f5c12376 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -85,6 +85,7 @@ import { modularWeaponSaleController } from "@/src/controllers/api/modularWeapon import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; import { nemesisController } from "@/src/controllers/api/nemesisController"; import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; +import { playedParkourTutorialController } from "@/src/controllers/api/playedParkourTutorialController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; @@ -177,6 +178,7 @@ apiRouter.get("/logout.php", logoutController); apiRouter.get("/marketRecommendations.php", marketRecommendationsController); apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController); apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); +apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index e29ea0a5..90b26303 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -178,7 +178,9 @@ export const addStartingGear = async ( combineInventoryChanges(inventoryChanges, inventoryDelta); } - inventory.PlayedParkourTutorial = true; + if (inventory.ReceivedStartingGear) { + logger.warn(`account already had starting gear but asked for it again?!`); + } inventory.ReceivedStartingGear = true; return inventoryChanges; From 9b330ffd3e53b63cc3b406f2fcff63cc794ec79a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:15:03 -0700 Subject: [PATCH 448/776] feat: sendMsgToInBox (#1580) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1580 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/sendMsgToInBoxController.ts | 31 +++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 src/controllers/api/sendMsgToInBoxController.ts diff --git a/src/controllers/api/sendMsgToInBoxController.ts b/src/controllers/api/sendMsgToInBoxController.ts new file mode 100644 index 00000000..7cad8c15 --- /dev/null +++ b/src/controllers/api/sendMsgToInBoxController.ts @@ -0,0 +1,31 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { createMessage } from "@/src/services/inboxService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const sendMsgToInBoxController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + await createMessage(accountId, [ + { + sub: data.title, + msg: data.message, + sndr: data.sender ?? "/Lotus/Language/Bosses/Ordis", + icon: data.senderIcon, + highPriority: data.highPriority, + transmission: data.transmission, + att: data.attachments + } + ]); + res.end(); +}; + +interface ISendMsgToInBoxRequest { + title: string; + message: string; + sender?: string; + senderIcon?: string; + highPriority?: boolean; + transmission?: string; + attachments?: string[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index f5c12376..41a6cf0b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -102,6 +102,7 @@ import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutControll import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController"; import { sellController } from "@/src/controllers/api/sellController"; +import { sendMsgToInBoxController } from "@/src/controllers/api/sendMsgToInBoxController"; import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController"; import { setActiveShipController } from "@/src/controllers/api/setActiveShipController"; import { setAllianceGuildPermissionsController } from "@/src/controllers/api/setAllianceGuildPermissionsController"; @@ -267,6 +268,7 @@ apiRouter.post("/saveLoadout.php", saveLoadoutController); apiRouter.post("/saveSettings.php", saveSettingsController); apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController); apiRouter.post("/sell.php", sellController); +apiRouter.post("/sendMsgToInBox.php", sendMsgToInBoxController); apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController); From 5c6b4b5779a90a599c4db64a5194a58c509112fa Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:15:14 -0700 Subject: [PATCH 449/776] fix: set activation & expiry for eidolon/venus/deimos bounties (#1581) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1581 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, 2 insertions(+) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index f5dde681..dbe61652 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -321,6 +321,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }; for (const syndicateInfo of worldState.SyndicateMissions) { if (syndicateInfo.Jobs && syndicateInfo.Seed != bountyCycle) { + syndicateInfo.Activation.$date.$numberLong = bountyCycleStart.toString(10); + syndicateInfo.Expiry.$date.$numberLong = bountyCycleEnd.toString(10); syndicateInfo.Seed = bountyCycle; logger.debug(`refreshing jobs for ${syndicateInfo.Tag}`); const rng = new CRng(bountyCycle); From f0ee1e8aadbeed26e503cd9ac4a44d6de3f9579e Mon Sep 17 00:00:00 2001 From: hxedcl Date: Sat, 12 Apr 2025 14:33:20 -0700 Subject: [PATCH 450/776] feat(webui): Spanish translation --- static/webui/script.js | 2 +- static/webui/translations/es.js | 158 ++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 static/webui/translations/es.js diff --git a/static/webui/script.js b/static/webui/script.js index d6216a08..fd997bbb 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -131,7 +131,7 @@ function setActiveLanguage(lang) { document.querySelector("[data-lang=" + lang + "]").classList.add("active"); window.dictPromise = new Promise(resolve => { - const webui_lang = ["en", "ru", "fr", "de", "zh"].indexOf(lang) == -1 ? "en" : lang; + const webui_lang = ["en", "ru", "fr", "de", "zh", "es"].indexOf(lang) == -1 ? "en" : lang; let script = document.getElementById("translations"); if (script) document.documentElement.removeChild(script); diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js new file mode 100644 index 00000000..2a3f8a2e --- /dev/null +++ b/static/webui/translations/es.js @@ -0,0 +1,158 @@ +// Spanish translation by hxedcl +dict = { + general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`, + general_addButton: `Agregar`, + general_bulkActions: `Acciones masivas`, + code_nonValidAuthz: `Tus credenciales no son válidas.`, + code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`, + code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`, + code_archgun: `Archcañón`, + code_melee: `Cuerpo a cuerpo`, + code_pistol: `Pistola`, + code_rifle: `Rifle`, + code_shotgun: `Escopeta`, + code_kitgun: `Kitgun`, + code_zaw: `Zaw`, + code_moteAmp: `Amp Mota`, + code_amp: `Amp`, + code_kDrive: `K-Drive`, + code_legendaryCore: `Núcleo legendario`, + code_traumaticPeculiar: `Traumatismo peculiar`, + code_starter: `|MOD| (Defectuoso)`, + code_badItem: `(Impostor)`, + code_maxRank: `Rango máximo`, + code_rename: `Renombrar`, + code_renamePrompt: `Escribe tu nuevo nombre personalizado:`, + code_remove: `Quitar`, + code_addItemsConfirm: `¿Estás seguro de que deseas agregar |COUNT| objetos a tu cuenta?`, + code_succRankUp: `Ascenso exitoso.`, + code_noEquipmentToRankUp: `No hay equipo para ascender.`, + code_succAdded: `Agregado exitosamente.`, + code_succRemoved: `Eliminado exitosamente.`, + code_buffsNumber: `Cantidad de mejoras`, + code_cursesNumber: `Cantidad de maldiciones`, + code_rerollsNumber: `Cantidad de reintentos`, + code_viewStats: `Ver estadísticas`, + code_rank: `Rango`, + code_count: `Cantidad`, + code_focusAllUnlocked: `Todas las escuelas de enfoque ya están desbloqueadas.`, + code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`, + code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`, + code_succImport: `Importación exitosa.`, + code_gild: `Refinar`, + code_moa: `Moa`, + code_zanuka: `Sabueso`, + code_zanukaA: `Sabueso Dorma`, + code_zanukaB: `Sabueso Bhaira`, + code_zanukaC: `Sabueso Hec`, + code_stage: `Etapa`, + code_complete: `Completa`, + code_nextStage: `Siguiente etapa`, + code_prevStage: `Etapa anterior`, + code_reset: `Reiniciar`, + code_setInactive: `Marcar la misión como inactiva`, + code_completed: `Completada`, + code_active: `Activa`, + code_pigment: `Pigmento`, + login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`, + login_emailLabel: `Dirección de correo electrónico`, + login_passwordLabel: `Contraseña`, + login_loginButton: `Iniciar sesión`, + navbar_logout: `Cerrar sesión`, + navbar_renameAccount: `Renombrar cuenta`, + navbar_deleteAccount: `Eliminar cuenta`, + navbar_inventory: `Inventario`, + navbar_mods: `Mods`, + navbar_quests: `Misiones`, + navbar_cheats: `Trucos`, + navbar_import: `Importar`, + inventory_addItems: `Agregar objetos`, + inventory_suits: `Warframes`, + inventory_longGuns: `Armas primarias`, + inventory_pistols: `Armas secundarias`, + inventory_melee: `Armas cuerpo a cuerpo`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Armas primarias Archwing`, + inventory_spaceMelee: `Armas cuerpo a cuerpo Archwing`, + inventory_mechSuits: `Necramechs`, + inventory_sentinels: `Centinelas`, + inventory_sentinelWeapons: `Armas de centinela`, + inventory_operatorAmps: `Amps`, + inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, + inventory_bulkAddSuits: `Agregar Warframes faltantes`, + inventory_bulkAddWeapons: `Agregar armas faltantes`, + inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`, + inventory_bulkAddSpaceWeapons: `Agregar armas Archwing faltantes`, + inventory_bulkAddSentinels: `Agregar centinelas faltantes`, + inventory_bulkAddSentinelWeapons: `Agregar armas de centinela faltantes`, + inventory_bulkRankUpSuits: `Maximizar rango de todos los Warframes`, + inventory_bulkRankUpWeapons: `Maximizar rango de todas las armas`, + inventory_bulkRankUpSpaceSuits: `Maximizar rango de todos los Archwings`, + inventory_bulkRankUpSpaceWeapons: `Maximizar rango de todas las armas Archwing`, + inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`, + inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`, + + quests_list: `Misiones`, + quests_completeAll: `Completar todas las misiones`, + quests_resetAll: `Reiniciar todas las misiones`, + quests_giveAll: `Otorgar todas las misiones`, + + currency_RegularCredits: `Créditos`, + currency_PremiumCredits: `Platino`, + currency_FusionPoints: `Endo`, + currency_PrimeTokens: `Aya Real`, + currency_owned: `Tienes |COUNT|.`, + powersuit_archonShardsLabel: `Ranuras de Fragmento de Archón`, + powersuit_archonShardsDescription: `Puedes usar estas ranuras ilimitadas para aplicar una amplia variedad de mejoras`, + mods_addRiven: `Agregar Agrietado`, + mods_fingerprint: `Huella digital`, + mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, + mods_rivens: `Agrietados`, + mods_mods: `Mods`, + mods_bulkAddMods: `Agregar mods faltantes`, + cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega |DISPLAYNAME| a administratorNames en el archivo config.json.`, + cheats_server: `Servidor`, + cheats_skipTutorial: `Omitir tutorial`, + cheats_skipAllDialogue: `Omitir todos los diálogos`, + cheats_unlockAllScans: `Desbloquear todos los escaneos`, + cheats_unlockAllMissions: `Desbloquear todas las misiones`, + cheats_infiniteCredits: `Créditos infinitos`, + cheats_infinitePlatinum: `Platino infinito`, + cheats_infiniteEndo: `Endo infinito`, + cheats_infiniteRegalAya: `Aya Real infinita`, + cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`, + cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`, + cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`, + cheats_unlockAllFlavourItems: `Desbloquear todos los ítems estéticos`, + cheats_unlockAllSkins: `Desbloquear todas las apariencias`, + cheats_unlockAllCapturaScenes: `Desbloquear todas las escenas Captura`, + cheats_unlockAllDecoRecipes: `Desbloquear todas las recetas decorativas del dojo`, + cheats_universalPolarityEverywhere: `Polaridad universal en todas partes`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `Patatas en todas partes`, + cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`, + cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`, + cheats_noDailyStandingLimits: `Sin límite diario de reputación`, + cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, + cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, + cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, + cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, + cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, + cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, + cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`, + cheats_noDojoResearchCosts: `Sin costo de investigación del dojo`, + cheats_noDojoResearchTime: `Sin tiempo de investigación del dojo`, + cheats_fastClanAscension: `Ascenso rápido del clan`, + cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`, + cheats_saveSettings: `Guardar configuración`, + cheats_account: `Cuenta`, + cheats_unlockAllFocusSchools: `Desbloquear todas las escuelas de enfoque`, + cheats_helminthUnlockAll: `Subir al máximo el Helminto`, + cheats_intrinsicsUnlockAll: `Maximizar todos los intrínsecos`, + cheats_changeSupportedSyndicate: `Sindicatos disponibles`, + cheats_changeButton: `Cambiar`, + cheats_none: `Ninguno`, + import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador serán sobrescritos en tu cuenta.`, + import_submit: `Enviar`, + prettier_sucks_ass: `` +}; From 900c6e9a26fc8ef22be5edd575bf6d6ecbf83837 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 23:45:26 +0200 Subject: [PATCH 451/776] chore: enforce UNIX-style line endings --- .gitattributes | 2 +- static/webui/translations/es.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0936ecf2..b3bfce24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ # Auto detect text files and perform LF normalization -* text=auto +* text=auto eol=lf static/webui/libs/ linguist-vendored diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 2a3f8a2e..8cafec2d 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -1,5 +1,5 @@ -// Spanish translation by hxedcl -dict = { +// Spanish translation by hxedcl +dict = { general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`, general_addButton: `Agregar`, general_bulkActions: `Acciones masivas`, @@ -92,12 +92,12 @@ dict = { inventory_bulkRankUpSpaceWeapons: `Maximizar rango de todas las armas Archwing`, inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`, inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`, - + quests_list: `Misiones`, quests_completeAll: `Completar todas las misiones`, quests_resetAll: `Reiniciar todas las misiones`, quests_giveAll: `Otorgar todas las misiones`, - + currency_RegularCredits: `Créditos`, currency_PremiumCredits: `Platino`, currency_FusionPoints: `Endo`, @@ -154,5 +154,5 @@ dict = { cheats_none: `Ninguno`, import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador serán sobrescritos en tu cuenta.`, import_submit: `Enviar`, - prettier_sucks_ass: `` -}; + prettier_sucks_ass: `` +}; From e5e6f7963bb20ef87ec252708af20b9bc6cc78e8 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sat, 12 Apr 2025 14:46:24 -0700 Subject: [PATCH 452/776] chore(webui): update ru translation (#1598) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1598 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 2299f2c1..4fe18fe8 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -134,11 +134,11 @@ dict = { cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, - cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, + cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, - cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, + cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, From 2187d9cd7ef22391309a35add76c7f08503c587a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 23:54:38 +0200 Subject: [PATCH 453/776] chore(webui): note performance impact of archon shards --- scripts/update-translations.js | 6 +++--- static/webui/index.html | 5 ++++- static/webui/translations/de.js | 1 + static/webui/translations/en.js | 3 ++- static/webui/translations/es.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/zh.js | 1 + 8 files changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/update-translations.js b/scripts/update-translations.js index 568885e6..5351afaa 100644 --- a/scripts/update-translations.js +++ b/scripts/update-translations.js @@ -4,7 +4,7 @@ const fs = require("fs"); function extractStrings(content) { - const regex = /([a-zA-Z_]+): `([^`]*)`,/g; + const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g; let matches; const strings = {}; while ((matches = regex.exec(content)) !== null) { @@ -15,7 +15,7 @@ function extractStrings(content) { const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); const sourceStrings = extractStrings(source); -const sourceLines = source.split("\n"); +const sourceLines = source.substring(0, source.length - 1).split("\n"); fs.readdirSync("../static/webui/translations").forEach(file => { if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") { @@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => { fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); } }); - } else if (line.length) { + } else { fs.writeSync(fileHandle, line + "\n"); } }); diff --git a/static/webui/index.html b/static/webui/index.html index afd841cd..f46be9ab 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -406,7 +406,10 @@
-

+

+ + +

x diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index ea662e37..222eeec4 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -105,6 +105,7 @@ dict = { currency_owned: `Du hast |COUNT|.`, powersuit_archonShardsLabel: `Archon-Scherben-Slots`, powersuit_archonShardsDescription: `Du kannst diese unbegrenzten Slots nutzen, um eine Vielzahl von Verbesserungen anzuwenden.`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `Riven hinzufügen`, mods_fingerprint: `Fingerabdruck`, mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 700e8566..0962ee33 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -103,7 +103,8 @@ dict = { currency_PrimeTokens: `Regal Aya`, currency_owned: `You have |COUNT|.`, powersuit_archonShardsLabel: `Archon Shard Slots`, - powersuit_archonShardsDescription: `You can use these unlimited slots to apply a wide range of upgrades`, + powersuit_archonShardsDescription: `You can use these unlimited slots to apply a wide range of upgrades.`, + powersuit_archonShardsDescription2: `Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `Add Riven`, mods_fingerprint: `Fingerprint`, mods_fingerprintHelp: `Need help with the fingerprint?`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 8cafec2d..8ca358d8 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -105,6 +105,7 @@ dict = { currency_owned: `Tienes |COUNT|.`, powersuit_archonShardsLabel: `Ranuras de Fragmento de Archón`, powersuit_archonShardsDescription: `Puedes usar estas ranuras ilimitadas para aplicar una amplia variedad de mejoras`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `Agregar Agrietado`, mods_fingerprint: `Huella digital`, mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 03807992..77d2bcf4 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -105,6 +105,7 @@ dict = { currency_owned: `|COUNT| possédés.`, powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`, powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations.`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `Ajouter un riven`, mods_fingerprint: `Empreinte`, mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 4fe18fe8..20410d9e 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -105,6 +105,7 @@ dict = { currency_owned: `У тебя |COUNT|.`, powersuit_archonShardsLabel: `Ячейки осколков архонта`, powersuit_archonShardsDescription: `Вы можете использовать эти неограниченные ячейки для установки множества улучшений.`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `Добавить Мод Разлома`, mods_fingerprint: `Отпечаток`, mods_fingerprintHelp: `Нужна помощь с отпечатком?`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 7598b005..a793c9ec 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -105,6 +105,7 @@ dict = { currency_owned: `当前拥有 |COUNT|。`, powersuit_archonShardsLabel: `执刑官源力石槽位`, powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `添加裂罅MOD`, mods_fingerprint: `印记`, mods_fingerprintHelp: `需要印记相关的帮助?`, From 2eb28c4e8994a24254f4e924c2c333b9274e9b69 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 23:59:06 +0200 Subject: [PATCH 454/776] chore: fix unnecessary condition --- src/services/inventoryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 90b26303..caf4d020 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -963,7 +963,7 @@ export const addStanding = ( const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing; - if (!isMedallion || (isMedallion && syndicateMeta.medallionsCappedByDailyLimit)) { + if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) { if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); } From 2c53d174899da66529a8a971d410a459a1d107c8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:52:01 -0700 Subject: [PATCH 455/776] chore: update pe+ (#1604) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1604 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a09d9ca..a858a5d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.53", + "warframe-public-export-plus": "^0.5.54", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.53", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.53.tgz", - "integrity": "sha512-FjYeCJ5OxvPWyETnV33YOeX7weVVeMy451RY7uewwSvRbSNFTDhmhvbrLhfwykulUX4RPakfZr8nO0S0a6lGCA==" + "version": "0.5.54", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.54.tgz", + "integrity": "sha512-27r6qLErr3P8UVDiEzhDAs/BjdAS3vI2CQ58jSI+LClDlj6QL+y1jQe8va/npl3Ft2K8PywLkZ8Yso0j9YzvOA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 77358348..008d5a18 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.53", + "warframe-public-export-plus": "^0.5.54", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" From 18a13911ba992b6726c492c395251fb1495c31e0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:53:35 -0700 Subject: [PATCH 456/776] fix: handle content-encoding "e" (#1588) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1588 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index 160a93c8..c8b19583 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui"; const app = express(); app.use((req, _res, next) => { - // 38.5.0 introduced "ezip" for encrypted body blobs. + // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data). // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it. - if (req.headers["content-encoding"] == "ezip") { + if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { req.headers["content-encoding"] = undefined; } next(); From e0200b2111737113a48d63e1f539a790d7c1fe4e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:54:42 -0700 Subject: [PATCH 457/776] fix: universalPolarityEverywhere not affecting all plexus slots (#1589) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1589 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 2c659717..805df53d 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -202,7 +202,8 @@ export const getInventoryResponse = async ( if (config.universalPolarityEverywhere) { const Polarity: IPolarity[] = []; - for (let i = 0; i != 12; ++i) { + // 12 is needed for necramechs. 14 is needed for plexus/crewshipharness. + for (let i = 0; i != 14; ++i) { Polarity.push({ Slot: i, Value: ArtifactPolarity.Any From 0928b842ad9a13d153c0074575374be24cf78546 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:54:49 -0700 Subject: [PATCH 458/776] fix: handle acquisition of weapon slots via nightwave (#1591) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1591 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/syndicateSacrificeController.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index b20df3bf..36ad07cb 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -3,15 +3,9 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { - addItem, - addMiscItems, - combineInventoryChanges, - getInventory, - updateCurrency -} from "@/src/services/inventoryService"; +import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; +import { isStoreItem, toStoreItem } from "@/src/services/itemDataService"; export const syndicateSacrificeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -77,10 +71,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp res.NewEpisodeReward = true; const reward = ExportNightwave.rewards[index]; let rewardType = reward.uniqueName; - if (isStoreItem(rewardType)) { - rewardType = fromStoreItem(rewardType); + if (!isStoreItem(rewardType)) { + rewardType = toStoreItem(rewardType); } - combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1)); + combineInventoryChanges( + res.InventoryChanges, + (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges + ); } } From 6a97a0c7c81b161eb12408f11725c39da685435f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:55:01 -0700 Subject: [PATCH 459/776] chore: default ChallengesFixVersion to 6 (#1594) we don't set this field anywhere but it might be needed Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1594 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0cb698cc..32ce6dac 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1471,7 +1471,7 @@ const inventorySchema = new Schema( DuviriInfo: DuviriInfoSchema, Mailbox: MailboxSchema, HandlerPoints: Number, - ChallengesFixVersion: Number, + ChallengesFixVersion: { type: Number, default: 6 }, PlayedParkourTutorial: Boolean, ActiveLandscapeTraps: [Schema.Types.Mixed], RepVotes: [Schema.Types.Mixed], From 20326fdaa05df306a9e99ab9b72ebaeee7c199ce Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:08:14 +0200 Subject: [PATCH 460/776] chore(webui): strip etc from item name --- static/webui/script.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index fd997bbb..b56e2f6e 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -277,9 +277,7 @@ function fetchItemList() { } else { const nameSet = new Set(); items.forEach(item => { - if (item.name.includes(" ")) { - item.name = item.name.replace(" ", ""); - } + item.name = item.name.replace(/<.+>/g, "").trim(); if ("badReason" in item) { if (item.badReason == "starter") { item.name = loc("code_starter").split("|MOD|").join(item.name); From 37ccd33d5c7f877edb6eff94a6ecf363c8521f65 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sun, 13 Apr 2025 05:48:38 -0700 Subject: [PATCH 461/776] chore(webui): update to German translation (#1607) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1607 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 222eeec4..807c2e47 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -105,7 +105,7 @@ dict = { currency_owned: `Du hast |COUNT|.`, powersuit_archonShardsLabel: `Archon-Scherben-Slots`, powersuit_archonShardsDescription: `Du kannst diese unbegrenzten Slots nutzen, um eine Vielzahl von Verbesserungen anzuwenden.`, - powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, + powersuit_archonShardsDescription2: `Hinweis: Jede Archon-Scherbe benötigt beim Laden etwas Zeit, um angewendet zu werden.`, mods_addRiven: `Riven hinzufügen`, mods_fingerprint: `Fingerabdruck`, mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, From 92d2616dda782633b02ec3c8f3268a431b140a36 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:50:51 -0700 Subject: [PATCH 462/776] feat: handle duet encounter (#1592) Closes #1274 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1592 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/nemesisController.ts | 117 +++++++++++++++++- src/helpers/nemesisHelpers.ts | 92 +++++++++++++- src/services/missionInventoryUpdateService.ts | 17 +-- src/types/inventoryTypes/inventoryTypes.ts | 7 +- 4 files changed, 210 insertions(+), 23 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 34e6bd4a..6f789e98 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,10 +1,25 @@ -import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers"; +import { + consumeModCharge, + encodeNemesisGuess, + getInfNodes, + getNemesisPasscode, + IKnifeResponse +} from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { SRng } from "@/src/services/rngService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; -import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { + IInnateDamageFingerprint, + InventorySlot, + IUpgradeClient, + IWeaponSkinClient, + LoadoutIndex, + TEquipmentKey +} from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; @@ -49,7 +64,7 @@ export const nemesisController: RequestHandler = async (req, res) => { } else if ((req.query.mode as string) == "p") { const inventory = await getInventory(accountId, "Nemesis"); const body = getJSONfromString(String(req.body)); - const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction); + const passcode = getNemesisPasscode(inventory.Nemesis!); let guessResult = 0; if (inventory.Nemesis!.Faction == "FC_INFESTATION") { for (let i = 0; i != 3; ++i) { @@ -66,6 +81,88 @@ export const nemesisController: RequestHandler = async (req, res) => { } } res.json({ GuessResult: guessResult }); + } else if (req.query.mode == "r") { + const inventory = await getInventory( + accountId, + "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" + ); + const body = getJSONfromString(String(req.body)); + if (inventory.Nemesis!.Faction == "FC_INFESTATION") { + const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; + const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; + + // Add to GuessHistory + const result1 = passcode == guess[0] ? 0 : 1; + const result2 = passcode == guess[1] ? 0 : 1; + const result3 = passcode == guess[2] ? 0 : 1; + inventory.Nemesis!.GuessHistory.push( + encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3) + ); + + // Increase antivirus + let antivirusGain = 5; + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + const response: IKnifeResponse = {}; + for (const upgrade of body.knife!.AttachedUpgrades) { + switch (upgrade.ItemType) { + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + } + } + inventory.Nemesis!.HenchmenKilled += antivirusGain; + if (inventory.Nemesis!.HenchmenKilled >= 100) { + inventory.Nemesis!.HenchmenKilled = 100; + inventory.Nemesis!.InfNodes = [ + { + Node: "CrewBattleNode559", + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + } else { + inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0); + } + + await inventory.save(); + res.json(response); + } else { + const passcode = getNemesisPasscode(inventory.Nemesis!); + if (passcode[body.position] != body.guess) { + res.end(); + } else { + inventory.Nemesis!.Rank += 1; + inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank); + await inventory.save(); + res.json({ RankIncrease: 1 }); + } + } + } else if ((req.query.mode as string) == "rs") { + // report spawn; POST but no application data in body + const inventory = await getInventory(accountId, "Nemesis"); + inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount; + await inventory.save(); + res.json({ LastEnc: inventory.Nemesis!.LastEnc }); } else if ((req.query.mode as string) == "s") { const inventory = await getInventory(accountId, "Nemesis"); const body = getJSONfromString(String(req.body)); @@ -173,6 +270,20 @@ interface INemesisPrespawnCheckRequest { potency?: number[]; } +interface INemesisRequiemRequest { + guess: number; // grn/crp: 4 bits | coda: 3x 4 bits + position: number; // grn/crp: 0-2 | coda: 0 + // knife field provided for coda only + knife?: { + Item: IEquipmentClient; + Skins: IWeaponSkinClient[]; + ModSlot: number; + CustSlot: number; + AttachedUpgrades: IUpgradeClient[]; + HiddenWhenHolstered: boolean; + }; +} + const kuvaLichVersionSixWeapons = [ "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 839e0675..fff5e94e 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -1,6 +1,11 @@ import { ExportRegions } from "warframe-public-export-plus"; import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; import { SRng } from "@/src/services/rngService"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { logger } from "../utils/logger"; +import { IOid } from "../types/commonTypes"; +import { Types } from "mongoose"; +import { addMods } from "../services/inventoryService"; export const getInfNodes = (faction: string, rank: number): IInfNode[] => { const infNodes = []; @@ -33,12 +38,93 @@ const systemIndexes: Record = { }; // Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. -export const getNemesisPasscode = (fp: bigint, faction: string): number[] => { - const rng = new SRng(fp); +export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => { + const rng = new SRng(nemesis.fp); const passcode = [rng.randomInt(0, 7)]; - if (faction != "FC_INFESTATION") { + if (nemesis.Faction != "FC_INFESTATION") { passcode.push(rng.randomInt(0, 7)); passcode.push(rng.randomInt(0, 7)); } return passcode; }; + +export const encodeNemesisGuess = ( + symbol1: number, + result1: number, + symbol2: number, + result2: number, + symbol3: number, + result3: number +): number => { + return ( + (symbol1 & 0xf) | + ((result1 & 3) << 12) | + ((symbol2 << 4) & 0xff) | + ((result2 << 14) & 0xffff) | + ((symbol3 & 0xf) << 8) | + ((result3 & 3) << 16) + ); +}; + +export const decodeNemesisGuess = (val: number): number[] => { + return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3]; +}; + +export interface IKnifeResponse { + UpgradeIds?: string[]; + UpgradeTypes?: string[]; + UpgradeFingerprints?: { lvl: number }[]; + UpgradeNew?: boolean[]; + HasKnife?: boolean; +} + +export const consumeModCharge = ( + response: IKnifeResponse, + inventory: TInventoryDatabaseDocument, + upgrade: { ItemId: IOid; ItemType: string }, + dataknifeUpgrades: string[] +): void => { + response.UpgradeIds ??= []; + response.UpgradeTypes ??= []; + response.UpgradeFingerprints ??= []; + response.UpgradeNew ??= []; + response.HasKnife = true; + + if (upgrade.ItemId.$oid != "000000000000000000000000") { + const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!; + const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number }; + fingerprint.lvl += 1; + dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + + response.UpgradeIds.push(upgrade.ItemId.$oid); + response.UpgradeTypes.push(upgrade.ItemType); + response.UpgradeFingerprints.push(fingerprint); + response.UpgradeNew.push(false); + } else { + const id = new Types.ObjectId(); + inventory.Upgrades.push({ + _id: id, + ItemType: upgrade.ItemType, + UpgradeFingerprint: `{"lvl":1}` + }); + + addMods(inventory, [ + { + ItemType: upgrade.ItemType, + ItemCount: -1 + } + ]); + + const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType); + if (dataknifeRawUpgradeIndex != -1) { + dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString(); + } else { + logger.warn(`${upgrade.ItemType} not found in dataknife config`); + } + + response.UpgradeIds.push(id.toString()); + response.UpgradeTypes.push(upgrade.ItemType); + response.UpgradeFingerprints.push({ lvl: 1 }); + response.UpgradeNew.push(true); + } +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index e6492a6e..de488ec4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -699,25 +699,10 @@ export const addMissionRewards = async ( } if (inventory.Nemesis.Faction == "FC_INFESTATION") { - inventoryChanges.Nemesis.HenchmenKilled ??= 0; - inventoryChanges.Nemesis.MissionCount ??= 0; - - inventory.Nemesis.HenchmenKilled += 5; inventory.Nemesis.MissionCount += 1; - inventoryChanges.Nemesis.HenchmenKilled += 5; + inventoryChanges.Nemesis.MissionCount ??= 0; inventoryChanges.Nemesis.MissionCount += 1; - - if (inventory.Nemesis.HenchmenKilled >= 100) { - inventory.Nemesis.InfNodes = [ - { - Node: "CrewBattleNode559", - Influence: 1 - } - ]; - inventory.Nemesis.Weakened = true; - inventoryChanges.Nemesis.Weakened = true; - } } inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 63d1b315..39894564 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -157,6 +157,11 @@ export type TSolarMapRegion = //TODO: perhaps split response and database into their own files +export enum LoadoutIndex { + NORMAL = 0, + DATAKNIFE = 7 +} + export interface IDailyAffiliations { DailyAffiliation: number; DailyAffiliationPvp: number; @@ -220,7 +225,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu ActiveQuest: string; FlavourItems: IFlavourItem[]; LoadOutPresets: ILoadOutPresets; - CurrentLoadOutIds: IOid[]; //TODO: we store it in the database using this representation as well :/ + CurrentLoadOutIds: IOid[]; // we store it in the database using this representation as well :/ Missions: IMission[]; RandomUpgradesIdentified?: number; LastRegionPlayed: TSolarMapRegion; From b429eed46c5ddbaca4c8ba8091a99e88704edaa4 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:51:02 -0700 Subject: [PATCH 463/776] feat: bounty item reward (#1595) Re #388 same as before I think this only missing `Field Bounties` and `Arcana Isolation Vault` Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1595 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 141 +++++++++++++++++- 1 file changed, 133 insertions(+), 8 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index de488ec4..b506b37b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -732,7 +732,7 @@ export const addMissionRewards = async ( const endlessJob = syndicateEntry.Jobs.find(j => j.endless); if (endlessJob) { const index = rewardInfo.JobStage % endlessJob.xpAmounts.length; - const excess = Math.floor(rewardInfo.JobStage / endlessJob.xpAmounts.length); + const excess = Math.floor(rewardInfo.JobStage / (endlessJob.xpAmounts.length - 1)); medallionAmount = Math.floor(endlessJob.xpAmounts[index] * (1 + 0.15000001 * excess)); } } @@ -907,15 +907,140 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u let rotations: number[] = []; if (RewardInfo.jobId) { - if (RewardInfo.JobTier! >= 0) { - const id = RewardInfo.jobId.split("_")[3]; - const syndicateInfo = getWorldState().SyndicateMissions.find(x => x._id.$oid == id); - if (syndicateInfo) { - const jobInfo = syndicateInfo.Jobs![RewardInfo.JobTier!]; - rewardManifests = [jobInfo.rewards]; - rotations = [RewardInfo.JobStage!]; + if (RewardInfo.JobStage! >= 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, tierStr, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); + const tier = Number(tierStr); + let isEndlessJob = false; + if (syndicateId) { + const worldState = getWorldState(); + let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId); + if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); + + if (syndicateEntry && syndicateEntry.Jobs) { + let job = syndicateEntry.Jobs[tier]; + + if (syndicateEntry.Tag === "EntratiSyndicate") { + const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); + if (vault) job = vault; + // if ( + // [ + // "DeimosRuinsExterminateBounty", + // "DeimosRuinsEscortBounty", + // "DeimosRuinsMistBounty", + // "DeimosRuinsPurifyBounty", + // "DeimosRuinsSacBounty" + // ].some(ending => jobType.endsWith(ending)) + // ) { + // job.rewards = "TODO"; // Droptable for Arcana Isolation Vault + // } + if ( + [ + "DeimosEndlessAreaDefenseBounty", + "DeimosEndlessExcavateBounty", + "DeimosEndlessPurifyBounty" + ].some(ending => jobType.endsWith(ending)) + ) { + const endlessJob = syndicateEntry.Jobs.find(j => j.endless); + if (endlessJob) { + isEndlessJob = true; + job = endlessJob; + const excess = Math.floor(RewardInfo.JobStage! / (job.xpAmounts.length - 1)); + + const rotationIndexes = [0, 0, 1, 2]; + const rotationIndex = rotationIndexes[excess % rotationIndexes.length]; + const dropTable = [ + "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards", + "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableBRewards", + "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableCRewards" + ]; + job.rewards = dropTable[rotationIndex]; + } + } + } else if (syndicateEntry.Tag === "SolarisSyndicate") { + if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && RewardInfo.JobStage == 2) { + job = { + rewards: + "/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTierATableARewards", + masteryReq: 0, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [1000] + }; + RewardInfo.Q = false; // Just in case + } else { + const tierMap = { + Two: "B", + Three: "C", + Four: "D" + }; + + for (const [key, tier] of Object.entries(tierMap)) { + if (jobType.endsWith(`Heists/HeistProfitTakerBounty${key}`)) { + job = { + rewards: `/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTier${tier}TableARewards`, + masteryReq: 0, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [1000] + }; + RewardInfo.Q = false; // Just in case + break; + } + } + } + } + rewardManifests = [job.rewards]; + rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)]; + if ( + RewardInfo.Q && + (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && + !isEndlessJob + ) { + rewardManifests.push(job.rewards); + rotations.push(ExportRewards[job.rewards].length - 1); + } + } } } + } else if (RewardInfo.challengeMissionId) { + const rewardTables: Record = { + EntratiLabSyndicate: [ + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierATableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierBTableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierCTableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierDTableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierETableRewards" + ], + ZarimanSyndicate: [ + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierATableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierBTableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierDTableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierETableRewards" + ], + HexSyndicate: [ + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierABountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierBBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierCBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierDBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierEBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierFBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/InfestedLichBountyRewards" + ] + }; + + const [syndicateTag, tierStr] = RewardInfo.challengeMissionId.split("_"); + const tier = Number(tierStr); + + const rewardTable = rewardTables[syndicateTag][tier]; + + if (rewardTable) { + rewardManifests = [rewardTable]; + rotations = [0]; + } else { + logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`); + } } else if (RewardInfo.VaultsCracked) { // For Spy missions, e.g. 3 vaults cracked = A, B, C for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { From a75e0c59afb1dea4a63fc2e215bd497d4a440765 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:51:15 -0700 Subject: [PATCH 464/776] feat: personal research (#1602) This should be good enough for the railjack quest at least Closes #1599 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1602 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 239 ++++++++++++------- src/models/inventoryModels/inventoryModel.ts | 36 ++- src/types/inventoryTypes/inventoryTypes.ts | 14 +- 3 files changed, 194 insertions(+), 95 deletions(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 5b0b5374..55083d5a 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -32,11 +32,11 @@ import { logger } from "@/src/utils/logger"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const guild = await getGuildForRequestEx(req, inventory); const data = JSON.parse(String(req.body)) as TGuildTechRequest; if (data.Action == "Sync") { let needSave = false; const techProjects: ITechProjectClient[] = []; + const guild = await getGuildForRequestEx(req, inventory); if (guild.TechProjects) { for (const project of guild.TechProjects) { const techProject: ITechProjectClient = { @@ -59,110 +59,170 @@ export const guildTechController: RequestHandler = async (req, res) => { } res.json({ TechProjects: techProjects }); } else if (data.Action == "Start") { - if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { - res.status(400).send("-1").end(); - return; - } - const recipe = ExportDojoRecipes.research[data.RecipeType]; - guild.TechProjects ??= []; - if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { - const techProject = - guild.TechProjects[ - guild.TechProjects.push({ - ItemType: data.RecipeType, - ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), - ReqItems: recipe.ingredients.map(x => ({ - ItemType: x.ItemType, - ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) - })), - State: 0 - }) - 1 - ]; - setGuildTechLogState(guild, techProject.ItemType, 5); - if (config.noDojoResearchCosts) { - processFundedGuildTechProject(guild, techProject, recipe); - } else { - if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { - guild.ActiveDojoColorResearch = data.RecipeType; + if (data.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const recipe = ExportDojoRecipes.research[data.RecipeType]; + guild.TechProjects ??= []; + if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { + const techProject = + guild.TechProjects[ + guild.TechProjects.push({ + ItemType: data.RecipeType, + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), + ReqItems: recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) + })), + State: 0 + }) - 1 + ]; + setGuildTechLogState(guild, techProject.ItemType, 5); + if (config.noDojoResearchCosts) { + processFundedGuildTechProject(guild, techProject, recipe); + } else { + if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { + guild.ActiveDojoColorResearch = data.RecipeType; + } } } + await guild.save(); + res.end(); + } else { + const recipe = ExportDojoRecipes.research[data.RecipeType]; + const techProject = + inventory.PersonalTechProjects[ + inventory.PersonalTechProjects.push({ + State: 0, + ReqCredits: recipe.price, + ItemType: data.RecipeType, + ReqItems: recipe.ingredients + }) - 1 + ]; + await inventory.save(); + res.json({ + isPersonal: true, + action: "Start", + personalTech: techProject.toJSON() + }); } - await guild.save(); - res.end(); } else if (data.Action == "Contribute") { - if (!hasAccessToDojo(inventory)) { - res.status(400).send("-1").end(); - return; - } + if ((req.query.guildId as string) == "000000000000000000000000") { + const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!; - const guildMember = (await GuildMember.findOne( - { accountId, guildId: guild._id }, - "RegularCreditsContributed MiscItemsContributed" - ))!; + techProject.ReqCredits -= data.RegularCredits; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); - const contributions = data; - const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; - - if (contributions.VaultCredits) { - if (contributions.VaultCredits > techProject.ReqCredits) { - contributions.VaultCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.VaultCredits; - guild.VaultRegularCredits! -= contributions.VaultCredits; - } - - if (contributions.RegularCredits > techProject.ReqCredits) { - contributions.RegularCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.RegularCredits; - - guildMember.RegularCreditsContributed ??= 0; - guildMember.RegularCreditsContributed += contributions.RegularCredits; - - if (contributions.VaultMiscItems.length) { - for (const miscItem of contributions.VaultMiscItems) { + const miscItemChanges = []; + for (const miscItem of data.MiscItems) { const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); if (reqItem) { if (miscItem.ItemCount > reqItem.ItemCount) { miscItem.ItemCount = reqItem.ItemCount; } reqItem.ItemCount -= miscItem.ItemCount; - - const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; - vaultMiscItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); } } - } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; - const miscItemChanges = []; - for (const miscItem of contributions.MiscItems) { - const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); - if (reqItem) { - if (miscItem.ItemCount > reqItem.ItemCount) { - miscItem.ItemCount = reqItem.ItemCount; - } - reqItem.ItemCount -= miscItem.ItemCount; - miscItemChanges.push({ - ItemType: miscItem.ItemType, - ItemCount: miscItem.ItemCount * -1 - }); + techProject.HasContributions = true; - addGuildMemberMiscItemContribution(guildMember, miscItem); + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + techProject.State = 1; + const recipe = ExportDojoRecipes.research[techProject.ItemType]; + techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000); } + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + PersonalResearch: { $oid: data.ResearchId }, + PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined + }); + } else { + if (!hasAccessToDojo(inventory)) { + res.status(400).send("-1").end(); + return; + } + + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + + const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + + if (data.VaultCredits) { + if (data.VaultCredits > techProject.ReqCredits) { + data.VaultCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.VaultCredits; + guild.VaultRegularCredits! -= data.VaultCredits; + } + + if (data.RegularCredits > techProject.ReqCredits) { + data.RegularCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.RegularCredits; + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += data.RegularCredits; + + if (data.VaultMiscItems.length) { + for (const miscItem of data.VaultMiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; + vaultMiscItem.ItemCount -= miscItem.ItemCount; + } + } + } + + const miscItemChanges = []; + for (const miscItem of data.MiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); + + addGuildMemberMiscItemContribution(guildMember, miscItem); + } + } + addMiscItems(inventory, miscItemChanges); + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); + inventoryChanges.MiscItems = miscItemChanges; + + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + res.json({ + InventoryChanges: inventoryChanges, + Vault: getGuildVault(guild) + }); } - addMiscItems(inventory, miscItemChanges); - const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); - inventoryChanges.MiscItems = miscItemChanges; - - // Check if research is fully funded now. - await processGuildTechProjectContributionsUpdate(guild, techProject); - - await Promise.all([guild.save(), inventory.save(), guildMember.save()]); - res.json({ - InventoryChanges: inventoryChanges, - Vault: getGuildVault(guild) - }); } else if (data.Action.split(",")[0] == "Buy") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; @@ -190,6 +250,7 @@ export const guildTechController: RequestHandler = async (req, res) => { } }); } else if (data.Action == "Fabricate") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; @@ -206,6 +267,7 @@ export const guildTechController: RequestHandler = async (req, res) => { // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. res.json({ inventoryChanges: inventoryChanges }); } else if (data.Action == "Pause") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; @@ -217,6 +279,7 @@ export const guildTechController: RequestHandler = async (req, res) => { await removePigmentsFromGuildMembers(guild._id); res.end(); } else if (data.Action == "Unpause") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; @@ -239,7 +302,7 @@ type TGuildTechRequest = interface IGuildTechBasicRequest { Action: "Start" | "Fabricate" | "Pause" | "Unpause"; - Mode: "Guild"; + Mode: "Guild" | "Personal"; RecipeType: string; } @@ -251,7 +314,7 @@ interface IGuildTechBuyRequest { interface IGuildTechContributeRequest { Action: "Contribute"; - ResearchId: ""; + ResearchId: string; RecipeType: string; RegularCredits: number; MiscItems: IMiscItem[]; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 32ce6dac..a0eddb10 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -84,7 +84,9 @@ import { IInfNode, IDiscoveredMarker, IWeeklyMission, - ILockedWeaponGroupDatabase + ILockedWeaponGroupDatabase, + IPersonalTechProjectDatabase, + IPersonalTechProjectClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -498,7 +500,34 @@ const seasonChallengeHistorySchema = new Schema( { _id: false } ); -//TODO: check whether this is complete +const personalTechProjectSchema = new Schema({ + State: Number, + ReqCredits: Number, + ItemType: String, + ReqItems: { type: [typeCountSchema], default: undefined }, + HasContributions: Boolean, + CompletionDate: Date +}); + +personalTechProjectSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +personalTechProjectSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret, _options) { + delete ret._id; + delete ret.__v; + + const db = ret as IPersonalTechProjectDatabase; + const client = ret as IPersonalTechProjectClient; + + if (db.CompletionDate) { + client.CompletionDate = toMongoDate(db.CompletionDate); + } + } +}); + const playerSkillsSchema = new Schema( { LPP_SPACE: { type: Number, default: 0 }, @@ -1442,7 +1471,7 @@ const inventorySchema = new Schema( //Railjack craft //https://warframe.fandom.com/wiki/Rising_Tide - PersonalTechProjects: [Schema.Types.Mixed], + PersonalTechProjects: { type: [personalTechProjectSchema], default: [] }, //Modulars lvl and exp(Railjack|Duviri) //https://warframe.fandom.com/wiki/Intrinsics @@ -1585,6 +1614,7 @@ export type InventoryDocumentProps = { Drones: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; CrewShipSalvagedWeaponsSkins: Types.DocumentArray; + PersonalTechProjects: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 39894564..1146bef6 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -46,6 +46,7 @@ export interface IInventoryDatabase | "EntratiVaultCountResetDate" | "BrandedSuits" | "LockedWeaponGroup" + | "PersonalTechProjects" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -77,6 +78,7 @@ export interface IInventoryDatabase EntratiVaultCountResetDate?: Date; BrandedSuits?: Types.ObjectId[]; LockedWeaponGroup?: ILockedWeaponGroupDatabase; + PersonalTechProjects: IPersonalTechProjectDatabase[]; } export interface IQuestKeyDatabase { @@ -306,7 +308,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; Settings?: ISettings; - PersonalTechProjects: IPersonalTechProject[]; + PersonalTechProjects: IPersonalTechProjectClient[]; PlayerSkills: IPlayerSkills; CrewShipAmmo: ITypeCount[]; CrewShipWeaponSkins: IUpgradeClient[]; @@ -941,16 +943,20 @@ export interface IPersonalGoalProgress { ReceivedClanReward1?: boolean; } -export interface IPersonalTechProject { +export interface IPersonalTechProjectDatabase { State: number; ReqCredits: number; ItemType: string; ReqItems: ITypeCount[]; + HasContributions?: boolean; + CompletionDate?: Date; +} + +export interface IPersonalTechProjectClient extends Omit { CompletionDate?: IMongoDate; - ItemId: IOid; ProductCategory?: string; CategoryItemId?: IOid; - HasContributions?: boolean; + ItemId: IOid; } export interface IPlayerSkills { From 729ea0abff699057ca18274fe82b6f9e57cce080 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:51:27 -0700 Subject: [PATCH 465/776] fix: look ahead for key chain messages (#1603) This is required for the railjack quest: - request has ChainStage 1 when it wants message from index 3 - request has ChainStage 4 when it wants message from index 6 - ... Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1603 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/itemDataService.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index e071111f..6e0edd23 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -185,14 +185,15 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); } - const keyChainStage = chainStages[ChainStage]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!keyChainStage) { - throw new Error(`KeyChainStage ${ChainStage} not found`); + let i = ChainStage; + let chainStageMessage = chainStages[i].messageToSendWhenTriggered; + while (!chainStageMessage) { + if (++i >= chainStages.length) { + break; + } + chainStageMessage = chainStages[i].messageToSendWhenTriggered; } - const chainStageMessage = keyChainStage.messageToSendWhenTriggered; - if (!chainStageMessage) { throw new Error( `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist` From d281e929ae3dbf44f37e64819922affe3be6a2d4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:51:54 -0700 Subject: [PATCH 466/776] feat: noKimCooldowns cheat (#1605) Closes #1537 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1605 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .github/workflows/build.yml | 3 ++- config.json.example | 1 + src/controllers/api/saveDialogueController.ts | 5 ++++- src/services/configService.ts | 1 + static/webui/index.html | 4 ++++ 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/zh.js | 1 + 11 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24a2f307..a59167d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,11 +15,12 @@ jobs: - run: npm run verify - run: npm run lint:ci - run: npm run prettier + - run: npm run update-translations - name: Fail if there are uncommitted changes run: | if [[ -n "$(git status --porcelain)" ]]; then echo "Uncommitted changes detected:" git status - git diff + git --no-pager diff exit 1 fi diff --git a/config.json.example b/config.json.example index 626d2d7b..a813d093 100644 --- a/config.json.example +++ b/config.json.example @@ -32,6 +32,7 @@ "noArgonCrystalDecay": false, "noMasteryRankUpCooldown": false, "noVendorPurchaseLimits": true, + "noKimCooldowns": false, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": false, "noDecoBuildStage": false, diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 999e0293..7d7d6380 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,4 +1,5 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { config } from "@/src/services/configService"; import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -24,7 +25,9 @@ export const saveDialogueController: RequestHandler = async (req, res) => { throw new Error("bad inventory state"); } const inventoryChanges: IInventoryChanges = {}; - const tomorrowAt0Utc = (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + const tomorrowAt0Utc = config.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; inventory.DialogueHistory.Dialogues ??= []; const dialogue = getDialogue(inventory, request.DialogueName); dialogue.Rank = request.Rank; diff --git a/src/services/configService.ts b/src/services/configService.ts index 7a3f0b1d..c584a1fb 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -38,6 +38,7 @@ interface IConfig { noArgonCrystalDecay?: boolean; noMasteryRankUpCooldown?: boolean; noVendorPurchaseLimits?: boolean; + noKimCooldowns?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; noDojoDecoBuildStage?: boolean; diff --git a/static/webui/index.html b/static/webui/index.html index f46be9ab..38eb3e54 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -607,6 +607,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 807c2e47..95870e31 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -137,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 0962ee33..3c9db66f 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -136,6 +136,7 @@ dict = { cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, + cheats_noKimCooldowns: `No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 8ca358d8..4656a328 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -137,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 77d2bcf4..90188be4 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -137,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 20410d9e..8682cca4 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -137,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index a793c9ec..2f720067 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -137,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, From aacd089123bc1c97ca392ddf412a0019e5799d54 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 13 Apr 2025 05:52:21 -0700 Subject: [PATCH 467/776] feat: caliber chicks 2 rewards (#1606) Closes #1263 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1606 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index caf4d020..d74bd9af 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -996,6 +996,10 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr } ]; addMiscItems(inventory, inventoryChanges.MiscItems); + } else if (node == "BeatCaliberChicks") { + await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/BeatCaliberChicksEmailItem", inventoryChanges); + } else if (node == "ClearedFiveLoops") { + await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/ClearedFiveLoopsEmailItem", inventoryChanges); } } From bef3aeed728e5ceb45532f0504ae5f2efff08a71 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sun, 13 Apr 2025 09:51:18 -0700 Subject: [PATCH 468/776] chore(webui): update to German translation (#1615) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1615 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 95870e31..31f9dc32 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -137,7 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, - cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, + cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, From 7736a2bf65aab110ad5992efa9063d1fccfd6ce5 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Sun, 13 Apr 2025 09:51:27 -0700 Subject: [PATCH 469/776] chore(webui): update to Spanish translation (#1616) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1616 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 4656a328..e67352cb 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -105,7 +105,7 @@ dict = { currency_owned: `Tienes |COUNT|.`, powersuit_archonShardsLabel: `Ranuras de Fragmento de Archón`, powersuit_archonShardsDescription: `Puedes usar estas ranuras ilimitadas para aplicar una amplia variedad de mejoras`, - powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, + powersuit_archonShardsDescription2: `Ten en cuenta que cada fragmento de archón tarda un poco en aplicarse al cargar`, mods_addRiven: `Agregar Agrietado`, mods_fingerprint: `Huella digital`, mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, @@ -137,7 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, - cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, + cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, From 9472f855b60ca82e6715fa31703147c933216bfc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:13:36 -0700 Subject: [PATCH 470/776] chore: slightly generalise auto-generation of vendor manifests (#1611) was gonna use this for the iron wake vendor manifest but the order is all wrong so in that way preprocessing remains a more preferable approach Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1611 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/rngService.ts | 6 ++ src/services/serversideVendorsService.ts | 115 ++++++++++++++++------- src/types/vendorTypes.ts | 10 +- 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/services/rngService.ts b/src/services/rngService.ts index b98f7bd3..983977ce 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -109,6 +109,12 @@ export class CRng { randomReward(pool: T[]): T | undefined { return getRewardAtPercentage(pool, this.random()); } + + churnSeed(its: number): void { + while (its--) { + this.state = (this.state * 1103515245 + 12345) & 0x7fffffff; + } + } } // Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth. diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 961b5faa..d1dc39e0 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -3,9 +3,15 @@ import path from "path"; import { repoDir } from "@/src/helpers/pathHelper"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; -import { IItemManifestPreprocessed, IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; +import { + IItemManifestPreprocessed, + IRawVendorManifest, + IVendorInfo, + IVendorManifestPreprocessed +} from "@/src/types/vendorTypes"; import { JSONParse } from "json-with-bigint"; import { ExportVendors } from "warframe-public-export-plus"; +import { unixTimesInMs } from "../constants/timeConstants"; const getVendorManifestJson = (name: string): IRawVendorManifest => { return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); @@ -44,14 +50,37 @@ const rawVendorManifests: IRawVendorManifest[] = [ getVendorManifestJson("ZarimanCommisionsManifestArchimedean") ]; +interface IGeneratableVendorInfo extends Omit { + cycleDuration?: number; +} + +const generatableVendors: IGeneratableVendorInfo[] = [ + { + _id: { $oid: "67dadc30e4b6e0e5979c8d84" }, + TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56", + RandomSeedType: "VRST_WEAPON", + RequiredGoalTag: "", + WeaponUpgradeValueAttenuationExponent: 2.25, + cycleDuration: 4 * unixTimesInMs.day + } + // { + // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, + // TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest", + // PropertyTextHash: "62B64A8065B7C0FA345895D4BC234621" + // } +]; + export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { return preprocessVendorManifest(vendorManifest); } } - if (typeName == "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest") { - return generateCodaWeaponVendorManifest(); + for (const vendorInfo of generatableVendors) { + if (vendorInfo.TypeName == typeName) { + return generateVendorManifest(vendorInfo); + } } return undefined; }; @@ -62,8 +91,10 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed return preprocessVendorManifest(vendorManifest); } } - if (oid == "67dadc30e4b6e0e5979c8d84") { - return generateCodaWeaponVendorManifest(); + for (const vendorInfo of generatableVendors) { + if (vendorInfo._id.$oid == oid) { + return generateVendorManifest(vendorInfo); + } } return undefined; }; @@ -103,41 +134,59 @@ const refreshExpiry = (expiry: IMongoDate): number => { return 0; }; -const generateCodaWeaponVendorManifest = (): IVendorManifestPreprocessed => { - const EPOCH = 1740960000 * 1000; - const DUR = 4 * 86400 * 1000; - const cycle = Math.trunc((Date.now() - EPOCH) / DUR); - const cycleStart = EPOCH + cycle * DUR; - const cycleEnd = cycleStart + DUR; - const binThisCycle = cycle % 2; // isOneBinPerCycle +const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { + const EPOCH = 1740960000 * 1000; // Monday; aligns with coda weapons 8 day cycle. + const manifest = ExportVendors[vendorInfo.TypeName]; + let binThisCycle; + if (manifest.isOneBinPerCycle) { + const cycleDuration = vendorInfo.cycleDuration!; // manifest.items[0].durationHours! * 3600_000; + const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); + binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. + } const items: IItemManifestPreprocessed[] = []; - const manifest = ExportVendors["/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest"]; - const rng = new CRng(cycle); - for (const rawItem of manifest.items) { - if (rawItem.bin != binThisCycle) { + let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; + for (let i = 0; i != manifest.items.length; ++i) { + const rawItem = manifest.items[i]; + if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) { continue; } - items.push({ - StoreItem: rawItem.storeItem, - ItemPrices: rawItem.itemPrices!.map(item => ({ ...item, ProductCategory: "MiscItems" })), - Bin: "BIN_" + rawItem.bin, - QuantityMultiplier: 1, - Expiry: { $date: { $numberLong: cycleEnd.toString() } }, - AllowMultipurchase: false, - LocTagRandSeed: (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)), - Id: { $oid: "67e9da12793a120d" + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") } - }); + const cycleDuration = vendorInfo.cycleDuration!; // rawItem.durationHours! * 3600_000; + const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); + const cycleStart = EPOCH + cycleIndex * cycleDuration; + const cycleEnd = cycleStart + cycleDuration; + if (soonestOfferExpiry > cycleEnd) { + soonestOfferExpiry = cycleEnd; + } + const rng = new CRng(cycleIndex); + rng.churnSeed(i); + /*for (let j = -1; j != rawItem.duplicates; ++j)*/ { + const item: IItemManifestPreprocessed = { + StoreItem: rawItem.storeItem, + ItemPrices: rawItem.itemPrices!.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), + Bin: "BIN_" + rawItem.bin, + QuantityMultiplier: 1, + Expiry: { $date: { $numberLong: cycleEnd.toString() } }, + AllowMultipurchase: false, + Id: { + $oid: + i.toString(16).padStart(8, "0") + + vendorInfo._id.$oid.substring(8, 16) + + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") + } + }; + if (vendorInfo.RandomSeedType) { + item.LocTagRandSeed = + (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)); + } + items.push(item); + } } + delete vendorInfo.cycleDuration; return { VendorInfo: { - _id: { $oid: "67dadc30e4b6e0e5979c8d84" }, - TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + ...vendorInfo, ItemManifest: items, - PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56", - RandomSeedType: "VRST_WEAPON", - RequiredGoalTag: "", - WeaponUpgradeValueAttenuationExponent: 2.25, - Expiry: { $date: { $numberLong: cycleEnd.toString() } } + Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } } } }; }; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index f962a494..2967a1e5 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -1,16 +1,16 @@ import { IMongoDate, IOid } from "./commonTypes"; -interface IItemPrice { +export interface IItemPrice { ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. ItemCount: number; ProductCategory: string; } -interface IItemPricePreprocessed extends Omit { +export interface IItemPricePreprocessed extends Omit { ItemType: string; } -interface IItemManifest { +export interface IItemManifest { StoreItem: string; ItemPrices?: IItemPrice[]; Bin: string; @@ -27,7 +27,7 @@ export interface IItemManifestPreprocessed extends Omit { +export interface IVendorInfoPreprocessed extends Omit { ItemManifest: IItemManifestPreprocessed[]; } From 4a971841a1e17be925dcd9670f755af1865ba686 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:13:51 -0700 Subject: [PATCH 471/776] fix: check addItems quantity for Drones & EmailItems (#1612) Closes #1610 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1612 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index d74bd9af..6357d404 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -381,7 +381,7 @@ export const addItem = async ( } else if (ExportResources[typeName].productCategory == "KubrowPetEggs") { const changes: IKubrowPetEggClient[] = []; if (quantity < 0 || quantity > 100) { - throw new Error(`unexpected acquisition quantity of KubrowPetEggs: ${quantity}`); + throw new Error(`unexpected acquisition quantity of KubrowPetEggs: got ${quantity}, expected 0..100`); } for (let i = 0; i != quantity; ++i) { const egg: IKubrowPetEggDatabase = { @@ -548,9 +548,18 @@ export const addItem = async ( } } if (typeName in ExportDrones) { - return addDrone(inventory, typeName); + // Can only get 1 at a time from crafting, but for convenience's sake, allow up 100 to via the WebUI. + if (quantity < 0 || quantity > 100) { + throw new Error(`unexpected acquisition quantity of Drones: got ${quantity}, expected 0..100`); + } + for (let i = 0; i != quantity; ++i) { + return addDrone(inventory, typeName); + } } if (typeName in ExportEmailItems) { + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of EmailItems: got ${quantity}, expected 1`); + } return await addEmailItem(inventory, typeName); } From c8ae3d688f6dd817aa22e939500b0f40462f5564 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:14:15 -0700 Subject: [PATCH 472/776] feat: noResourceExtractorDronesDamage cheat (#1613) Closes #1609 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1613 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + src/controllers/api/dronesController.ts | 2 +- src/services/configService.ts | 1 + static/webui/index.html | 4 ++++ 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/zh.js | 1 + 10 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index a813d093..907f8af7 100644 --- a/config.json.example +++ b/config.json.example @@ -34,6 +34,7 @@ "noVendorPurchaseLimits": true, "noKimCooldowns": false, "instantResourceExtractorDrones": false, + "noResourceExtractorDronesDamage": false, "noDojoRoomBuildStage": false, "noDecoBuildStage": false, "fastDojoRoomDestruction": false, diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index 97e0d478..f319773b 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -55,7 +55,7 @@ export const dronesController: RequestHandler = async (req, res) => { ? new Date() : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); drone.PendingDamage = - Math.random() < system.damageChance + !config.noResourceExtractorDronesDamage && Math.random() < system.damageChance ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) : 0; const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!; diff --git a/src/services/configService.ts b/src/services/configService.ts index c584a1fb..88a6634a 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -40,6 +40,7 @@ interface IConfig { noVendorPurchaseLimits?: boolean; noKimCooldowns?: boolean; instantResourceExtractorDrones?: boolean; + noResourceExtractorDronesDamage?: boolean; noDojoRoomBuildStage?: boolean; noDojoDecoBuildStage?: boolean; fastDojoRoomDestruction?: boolean; diff --git a/static/webui/index.html b/static/webui/index.html index 38eb3e54..d4a900c3 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -615,6 +615,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 31f9dc32..047b3b7d 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -139,6 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, + cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 3c9db66f..01a97404 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -138,6 +138,7 @@ dict = { cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_noKimCooldowns: `No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, + cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index e67352cb..b918d6c0 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -139,6 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, + cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 90188be4..e2ebd5e7 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -139,6 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, + cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 8682cca4..1def5df7 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -139,6 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, + cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 2f720067..1ee2ea4c 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -139,6 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, + cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `快速拆除道场房间`, From c64d466ce14d76da173516063f034a5a3b56197a Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Mon, 14 Apr 2025 07:14:50 -0700 Subject: [PATCH 473/776] fix: universalPolarityEverywhere not applying on plexus aura slot (#1614) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1614 Reviewed-by: Sainan Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- src/controllers/api/inventoryController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 805df53d..bf71ae42 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -202,8 +202,8 @@ export const getInventoryResponse = async ( if (config.universalPolarityEverywhere) { const Polarity: IPolarity[] = []; - // 12 is needed for necramechs. 14 is needed for plexus/crewshipharness. - for (let i = 0; i != 14; ++i) { + // 12 is needed for necramechs. 15 is needed for plexus/crewshipharness. + for (let i = 0; i != 15; ++i) { Polarity.push({ Slot: i, Value: ArtifactPolarity.Any From 827ea47468a83f8e1f5dc6057647ae79a248cc9d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:16:25 -0700 Subject: [PATCH 474/776] feat: personal quarters loadout, stencil, vignette, & fish customisation (#1619) Closes #1618 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1619 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/getShipController.ts | 5 +- .../api/setShipFavouriteLoadoutController.ts | 35 +++++++++----- .../api/setShipVignetteController.ts | 48 +++++++++++++++++++ src/models/personalRoomsModel.ts | 44 +++++++++-------- src/routes/api.ts | 2 + src/types/personalRoomsTypes.ts | 11 +++-- src/types/shipTypes.ts | 15 +++++- 7 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 src/controllers/api/setShipVignetteController.ts diff --git a/src/controllers/api/getShipController.ts b/src/controllers/api/getShipController.ts index 10f6e25f..ac1ebadf 100644 --- a/src/controllers/api/getShipController.ts +++ b/src/controllers/api/getShipController.ts @@ -26,7 +26,10 @@ export const getShipController: RequestHandler = async (req, res) => { Colors: personalRooms.ShipInteriorColors, ShipAttachments: ship.ShipAttachments, SkinFlavourItem: ship.SkinFlavourItem - } + }, + FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId + ? toOid(personalRooms.Ship.FavouriteLoadoutId) + : undefined }, Apartment: personalRooms.Apartment, TailorShop: personalRooms.TailorShop diff --git a/src/controllers/api/setShipFavouriteLoadoutController.ts b/src/controllers/api/setShipFavouriteLoadoutController.ts index d798e0ed..e4bf2e13 100644 --- a/src/controllers/api/setShipFavouriteLoadoutController.ts +++ b/src/controllers/api/setShipFavouriteLoadoutController.ts @@ -3,29 +3,40 @@ import { RequestHandler } from "express"; import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { IOid } from "@/src/types/commonTypes"; import { Types } from "mongoose"; +import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes"; export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const personalRooms = await getPersonalRooms(accountId); const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest; - if (body.BootLocation != "SHOP") { - throw new Error(`unexpected BootLocation: ${body.BootLocation}`); - } - const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName); - if (display) { - display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); + if (body.BootLocation == "LISET") { + personalRooms.Ship.FavouriteLoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); + } else if (body.BootLocation == "APARTMENT") { + updateTaggedDisplay(personalRooms.Apartment.FavouriteLoadouts, body); + } else if (body.BootLocation == "SHOP") { + updateTaggedDisplay(personalRooms.TailorShop.FavouriteLoadouts, body); } else { - personalRooms.TailorShop.FavouriteLoadouts.push({ - Tag: body.TagName, - LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid) - }); + console.log(body); + throw new Error(`unexpected BootLocation: ${body.BootLocation}`); } await personalRooms.save(); res.json({}); }; interface ISetShipFavouriteLoadoutRequest { - BootLocation: string; + BootLocation: TBootLocation; FavouriteLoadoutId: IOid; - TagName: string; + TagName?: string; } + +const updateTaggedDisplay = (arr: IFavouriteLoadoutDatabase[], body: ISetShipFavouriteLoadoutRequest): void => { + const display = arr.find(x => x.Tag == body.TagName!); + if (display) { + display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); + } else { + arr.push({ + Tag: body.TagName!, + LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid) + }); + } +}; diff --git a/src/controllers/api/setShipVignetteController.ts b/src/controllers/api/setShipVignetteController.ts new file mode 100644 index 00000000..a1d991da --- /dev/null +++ b/src/controllers/api/setShipVignetteController.ts @@ -0,0 +1,48 @@ +import { addMiscItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const setShipVignetteController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "MiscItems"); + const personalRooms = await getPersonalRooms(accountId); + const body = JSON.parse(String(req.body)) as ISetShipVignetteRequest; + personalRooms.Ship.Wallpaper = body.Wallpaper; + personalRooms.Ship.Vignette = body.Vignette; + personalRooms.Ship.VignetteFish ??= []; + const inventoryChanges: IInventoryChanges = {}; + for (let i = 0; i != body.Fish.length; ++i) { + if (body.Fish[i] && !personalRooms.Ship.VignetteFish[i]) { + logger.debug(`moving ${body.Fish[i]} from inventory to vignette slot ${i}`); + const miscItemsDelta = [{ ItemType: body.Fish[i], ItemCount: -1 }]; + addMiscItems(inventory, miscItemsDelta); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta }); + } else if (personalRooms.Ship.VignetteFish[i] && !body.Fish[i]) { + logger.debug(`moving ${personalRooms.Ship.VignetteFish[i]} from vignette slot ${i} to inventory`); + const miscItemsDelta = [{ ItemType: personalRooms.Ship.VignetteFish[i], ItemCount: +1 }]; + addMiscItems(inventory, miscItemsDelta); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta }); + } + } + personalRooms.Ship.VignetteFish = body.Fish; + if (body.VignetteDecos.length) { + logger.error(`setShipVignette request not fully handled:`, body); + } + await Promise.all([inventory.save(), personalRooms.save()]); + res.json({ + Wallpaper: body.Wallpaper, + Vignette: body.Vignette, + VignetteFish: body.Fish, + InventoryChanges: inventoryChanges + }); +}; + +interface ISetShipVignetteRequest { + Wallpaper: string; + Vignette: string; + Fish: string[]; + VignetteDecos: unknown[]; +} diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index 1c6a7c6d..0fcdda72 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -2,13 +2,13 @@ import { toOid } from "@/src/helpers/inventoryHelpers"; import { colorSchema } from "@/src/models/inventoryModels/inventoryModel"; import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes"; import { - IApartment, IFavouriteLoadoutDatabase, IGardening, IPlacedDecosDatabase, IPictureFrameInfo, IRoom, - ITailorShopDatabase + ITailorShopDatabase, + IApartmentDatabase } from "@/src/types/shipTypes"; import { Schema, model } from "mongoose"; @@ -62,19 +62,34 @@ const roomSchema = new Schema( { _id: false } ); +const favouriteLoadoutSchema = new Schema( + { + Tag: String, + LoadoutId: Schema.Types.ObjectId + }, + { _id: false } +); +favouriteLoadoutSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + returnedObject.LoadoutId = toOid(returnedObject.LoadoutId); + } +}); + const gardeningSchema = new Schema({ Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening }); -const apartmentSchema = new Schema( +const apartmentSchema = new Schema( { Rooms: [roomSchema], - FavouriteLoadouts: [Schema.Types.Mixed], + FavouriteLoadouts: [favouriteLoadoutSchema], Gardening: gardeningSchema // TODO: ensure this is correct }, { _id: false } ); -const apartmentDefault: IApartment = { +const apartmentDefault: IApartmentDatabase = { Rooms: [ { Name: "ElevatorLanding", MaxCapacity: 1600 }, { Name: "ApartmentRoomA", MaxCapacity: 1000 }, @@ -90,6 +105,10 @@ const orbiterSchema = new Schema( { Features: [String], Rooms: [roomSchema], + VignetteFish: { type: [String], default: undefined }, + FavouriteLoadoutId: Schema.Types.ObjectId, + Wallpaper: String, + Vignette: String, ContentUrlSignature: { type: String, required: false }, BootLocation: String }, @@ -107,21 +126,6 @@ const orbiterDefault: IOrbiter = { ] }; -const favouriteLoadoutSchema = new Schema( - { - Tag: String, - LoadoutId: Schema.Types.ObjectId - }, - { _id: false } -); -favouriteLoadoutSchema.set("toJSON", { - virtuals: true, - transform(_document, returnedObject) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - returnedObject.LoadoutId = toOid(returnedObject.LoadoutId); - } -}); - const tailorShopSchema = new Schema( { FavouriteLoadouts: [favouriteLoadoutSchema], diff --git a/src/routes/api.ts b/src/routes/api.ts index 41a6cf0b..bd3ffd4b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -115,6 +115,7 @@ import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdContro import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; +import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController"; import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController"; import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController"; import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController"; @@ -277,6 +278,7 @@ apiRouter.post("/setGuildMotd.php", setGuildMotdController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); +apiRouter.post("/setShipVignette.php", setShipVignetteController); apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController); apiRouter.post("/shipDecorations.php", shipDecorationsController); apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController); diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index fb672955..68239bb4 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -5,13 +5,18 @@ import { IPlacedDecosDatabase, ITailorShop, ITailorShopDatabase, - TBootLocation + TBootLocation, + IApartmentDatabase } from "@/src/types/shipTypes"; import { Document, Model, Types } from "mongoose"; export interface IOrbiter { Features: string[]; Rooms: IRoom[]; + VignetteFish?: string[]; + FavouriteLoadoutId?: Types.ObjectId; + Wallpaper?: string; + Vignette?: string; ContentUrlSignature?: string; BootLocation?: TBootLocation; } @@ -28,7 +33,7 @@ export interface IPersonalRoomsDatabase { personalRoomsOwnerId: Types.ObjectId; activeShipId: Types.ObjectId; Ship: IOrbiter; - Apartment: IApartment; + Apartment: IApartmentDatabase; TailorShop: ITailorShopDatabase; } @@ -38,7 +43,7 @@ export type PersonalRoomsDocumentProps = { Ship: Omit & { Rooms: RoomsType[]; }; - Apartment: Omit & { + Apartment: Omit & { Rooms: RoomsType[]; }; TailorShop: Omit & { diff --git a/src/types/shipTypes.ts b/src/types/shipTypes.ts index 23c46c48..a097f3ca 100644 --- a/src/types/shipTypes.ts +++ b/src/types/shipTypes.ts @@ -28,8 +28,12 @@ export interface IShip { ShipId: IOid; ShipInterior: IShipInterior; Rooms: IRoom[]; - ContentUrlSignature?: string; + VignetteFish?: string[]; + FavouriteLoadoutId?: IOid; + Wallpaper?: string; + Vignette?: string; BootLocation?: TBootLocation; + ContentUrlSignature?: string; } export interface IShipDatabase { @@ -60,10 +64,17 @@ export interface IPlanters { export interface IGardening { Planters?: IPlanters[]; } + export interface IApartment { Gardening: IGardening; Rooms: IRoom[]; - FavouriteLoadouts: string[]; + FavouriteLoadouts: IFavouriteLoadout[]; +} + +export interface IApartmentDatabase { + Gardening: IGardening; + Rooms: IRoom[]; + FavouriteLoadouts: IFavouriteLoadoutDatabase[]; } export interface IPlacedDecosDatabase { From 8ebb7497327370d6f40bfa25c1858cde11c2c809 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Mon, 14 Apr 2025 11:45:00 -0700 Subject: [PATCH 475/776] chore(webui): update to Spanish translation (#1636) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1636 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index b918d6c0..2c303d80 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -139,7 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, + cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`, From 43f3917b0948a4c5e714734887a07299f367b56e Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:10:25 -0700 Subject: [PATCH 476/776] fix: additional checks in bounty rewards (#1626) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1626 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b506b37b..dd751926 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -922,7 +922,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); - if (vault) job = vault; + if (vault && locationTag) job = vault; // if ( // [ // "DeimosRuinsExterminateBounty", @@ -997,8 +997,10 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && !isEndlessJob ) { - rewardManifests.push(job.rewards); - rotations.push(ExportRewards[job.rewards].length - 1); + if (ExportRewards[job.rewards]) { + rewardManifests.push(job.rewards); + rotations.push(ExportRewards[job.rewards].length - 1); + } } } } @@ -1053,17 +1055,20 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); } - rewardManifests - .map(name => ExportRewards[name]) - .forEach(table => { - for (const rotation of rotations) { - const rotationRewards = table[rotation]; - const drop = getRandomRewardByChance(rotationRewards); - if (drop) { - drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); - } + rewardManifests.forEach(name => { + const table = ExportRewards[name]; + if (!table) { + logger.error(`unknown droptable: ${name}`); + return; + } + for (const rotation of rotations) { + const rotationRewards = table[rotation]; + const drop = getRandomRewardByChance(rotationRewards); + if (drop) { + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } - }); + } + }); if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) { const deck = ExportRewards[region.cacheRewardManifest]; From fa68a1357dc94e11e59ac767c57dfbc17cd291f3 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Tue, 15 Apr 2025 06:15:33 -0700 Subject: [PATCH 477/776] chore(webui): update to German translation (#1642) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1642 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 047b3b7d..7cca988a 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -139,7 +139,7 @@ dict = { cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, + cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, From bd837381689cc6e87b30053c9bb2727b8d9672b3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:15:49 -0700 Subject: [PATCH 478/776] fix: provide a response to setPlacedDecoInfo (#1632) This seems to be needed for the client when refreshing the ship after loading into a mission as it does not resync the ship otherwise. Closes #1629 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1632 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/setPlacedDecoInfoController.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/setPlacedDecoInfoController.ts b/src/controllers/api/setPlacedDecoInfoController.ts index 56b9afe7..19f76061 100644 --- a/src/controllers/api/setPlacedDecoInfoController.ts +++ b/src/controllers/api/setPlacedDecoInfoController.ts @@ -1,5 +1,5 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; +import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; import { RequestHandler } from "express"; import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService"; @@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest; await handleSetPlacedDecoInfo(accountId, payload); - res.end(); + res.json({ + DecoId: payload.DecoId, + IsPicture: true, + PictureFrameInfo: payload.PictureFrameInfo, + BootLocation: payload.BootLocation + } satisfies ISetPlacedDecoInfoResponse); }; + +interface ISetPlacedDecoInfoResponse { + DecoId: string; + IsPicture: boolean; + PictureFrameInfo?: IPictureFrameInfo; + BootLocation?: string; +} From 380f0662a407f33b38f42c57d1949d8eb2202851 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:07 -0700 Subject: [PATCH 479/776] fix: don't try to subtract MiscItems for polarity swap (#1633) Closes #1620 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1633 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/upgradesController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index 855d2aa7..2a376b4a 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => { operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" ) { updateCurrency(inventory, 10, true); - } else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") { + } else if ( + operation.OperationType != "UOT_SWAP_POLARITY" && + operation.OperationType != "UOT_ABILITY_OVERRIDE" + ) { + if (!operation.UpgradeRequirement) { + throw new Error(`${operation.OperationType} operation should be free?`); + } addMiscItems(inventory, [ { ItemType: operation.UpgradeRequirement, From 0c884576bd7cc2bdf5a37bff57484c88ce06d6dd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:19 -0700 Subject: [PATCH 480/776] feat: picking up prex cards (#1634) Closes #1621 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1634 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../giveShipDecoAndLoreFragmentController.ts | 20 +++++++++++++++++++ src/routes/api.ts | 2 ++ src/services/inventoryService.ts | 14 ++++++++++++- src/services/missionInventoryUpdateService.ts | 10 ++-------- 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 src/controllers/api/giveShipDecoAndLoreFragmentController.ts diff --git a/src/controllers/api/giveShipDecoAndLoreFragmentController.ts b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts new file mode 100644 index 00000000..08385cbf --- /dev/null +++ b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts @@ -0,0 +1,20 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations"); + const data = getJSONfromString(String(req.body)); + addLoreFragmentScans(inventory, data.LoreFragmentScans); + addShipDecorations(inventory, data.ShipDecorations); + await inventory.save(); + res.end(); +}; + +interface IGiveShipDecoAndLoreFragmentRequest { + LoreFragmentScans: ILoreFragmentScan[]; + ShipDecorations: ITypeCount[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index bd3ffd4b..1c69da55 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -62,6 +62,7 @@ import { gildWeaponController } from "@/src/controllers/api/gildWeaponController import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey"; +import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController"; import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; import { guildTechController } from "@/src/controllers/api/guildTechController"; import { hostSessionController } from "@/src/controllers/api/hostSessionController"; @@ -239,6 +240,7 @@ apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); +apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController); apiRouter.post("/giveStartingGear.php", giveStartingGearController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6357d404..48657e88 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -21,7 +21,8 @@ import { ICalendarProgress, IDroneClient, IUpgradeClient, - TPartialStartingGear + TPartialStartingGear, + ILoreFragmentScan } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -1350,6 +1351,17 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); }; +export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => { + arr.forEach(clientFragment => { + const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); + if (fragment) { + fragment.Progress += clientFragment.Progress; + } else { + inventory.LoreFragmentScans.push(clientFragment); + } + }); +}; + export const addChallenges = ( inventory: TInventoryDatabaseDocument, ChallengeProgress: IChallengeProgress[], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index dd751926..44923250 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -23,6 +23,7 @@ import { addGearExpByCategory, addItem, addLevelKeys, + addLoreFragmentScans, addMiscItems, addMissionComplete, addMods, @@ -291,14 +292,7 @@ export const addMissionInventoryUpdates = async ( break; } case "LoreFragmentScans": - value.forEach(clientFragment => { - const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); - if (fragment) { - fragment.Progress += clientFragment.Progress; - } else { - inventory.LoreFragmentScans.push(clientFragment); - } - }); + addLoreFragmentScans(inventory, value); break; case "LibraryScans": value.forEach(scan => { From a6d2c8b18afeb81947545f256f20c802faea35bc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:31 -0700 Subject: [PATCH 481/776] fix: don't give credits for junctions, the index, and free flight (#1635) Closes #1625 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1635 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 44923250..7fab571b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -586,7 +586,14 @@ export const addMissionRewards = async ( const node = ExportRegions[missions.Tag]; //node based credit rewards for mission completion - if (node.missionIndex !== 28) { + if ( + node.missionIndex != 23 && // junction + node.missionIndex != 28 && // open world + missions.Tag != "SolNode761" && // the index + missions.Tag != "SolNode762" && // the index + missions.Tag != "SolNode763" && // the index + missions.Tag != "CrewBattleNode556" // free flight + ) { const levelCreditReward = getLevelCreditRewards(node); missionCompletionCredits += levelCreditReward; inventory.RegularCredits += levelCreditReward; From d28437b6588184f8bdcd59175d888268561fd502 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:16:40 -0700 Subject: [PATCH 482/776] feat: give 5 steel essence when completing an SP incursion (#1637) Closes #1631 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1637 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7fab571b..b74c1ddf 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -899,6 +899,12 @@ function getLevelCreditRewards(node: IRegion): number { function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] { const drops: IMissionReward[] = []; + if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", + ItemCount: 5 + }); + } if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; let rewardManifests: string[] = From 3f0a2bec48c4a3e8790983f45d7ad457396c7f96 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:46:08 -0700 Subject: [PATCH 483/776] fix: generate rewards based on RewardSeed to match what's show in client (#1628) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1628 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/getNewRewardSeedController.ts | 2 -- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/missionInventoryUpdateService.ts | 18 +++++++++++++++--- src/services/rngService.ts | 6 +++++- src/types/inventoryTypes/inventoryTypes.ts | 2 +- src/types/requestTypes.ts | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index 4ff82405..cb9e1f82 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,14 +1,12 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { generateRewardSeed } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; export const getNewRewardSeedController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const rewardSeed = generateRewardSeed(); - logger.debug(`generated new reward seed: ${rewardSeed}`); await Inventory.updateOne( { accountOwnerId: accountId diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a0eddb10..9c737d24 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1213,7 +1213,7 @@ const inventorySchema = new Schema( accountOwnerId: Schema.Types.ObjectId, SubscribedToEmails: { type: Number, default: 0 }, SubscribedToEmailsPersonalized: { type: Number, default: 0 }, - RewardSeed: Number, + RewardSeed: BigInt, //Credit RegularCredits: { type: Number, default: 0 }, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b74c1ddf..813e7966 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -9,7 +9,7 @@ import { } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; +import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService"; import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addBooster, @@ -31,6 +31,7 @@ import { addShipDecorations, addStanding, combineInventoryChanges, + generateRewardSeed, updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; @@ -70,7 +71,12 @@ const getRotations = (rotationCount: number, tierOverride: number | undefined): return rotatedValues; }; -const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { +const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | undefined => { + if (rng) { + const res = rng.randomReward(pool as IRngResult[]); + rng.randomFloat(); // something related to rewards multiplier + return res; + } return getRandomReward(pool as IRngResult[]); }; @@ -548,6 +554,11 @@ export const addMissionRewards = async ( return { MissionRewards: [] }; } + if (rewardInfo.rewardSeed) { + // We're using a reward seed, so give the client a new one in the response. On live, missionInventoryUpdate seems to always provide a fresh one in the response. + inventory.RewardSeed = generateRewardSeed(); + } + //TODO: check double reward merging const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); logger.debug("random mission drops:", MissionRewards); @@ -1062,6 +1073,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); } + const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn); rewardManifests.forEach(name => { const table = ExportRewards[name]; if (!table) { @@ -1070,7 +1082,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u } for (const rotation of rotations) { const rotationRewards = table[rotation]; - const drop = getRandomRewardByChance(rotationRewards); + const drop = getRandomRewardByChance(rotationRewards, rng); if (drop) { drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 983977ce..f88b23c0 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -31,7 +31,7 @@ const getRewardAtPercentage = (pool: T[], per return item; } } - throw new Error("What the fuck?"); + return pool[pool.length - 1]; }; export const getRandomReward = (pool: T[]): T | undefined => { @@ -142,4 +142,8 @@ export class SRng { this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645; } + + randomReward(pool: T[]): T | undefined { + return getRewardAtPercentage(pool, this.randomFloat()); + } } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 1146bef6..63207ef0 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -194,7 +194,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Mailbox?: IMailboxClient; SubscribedToEmails: number; Created: IMongoDate; - RewardSeed: number; + RewardSeed: number | bigint; RegularCredits: number; PremiumCredits: number; PremiumCreditsFree: number; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 494e23f4..fff164a8 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -141,7 +141,7 @@ export interface IRewardInfo { EOM_AFK?: number; rewardQualifications?: string; // did a Survival for 5 minutes and this was "1" PurgatoryRewardQualifications?: string; - rewardSeed?: number; + rewardSeed?: number | bigint; periodicMissionTag?: string; // for bounties, only EOM_AFK and node are given from above, plus: From 28d7ca8ca0f24f7ebf05f403f806ecd9fd3dd0e7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:48:17 +0200 Subject: [PATCH 484/776] chore: address eslint warnings --- src/services/missionInventoryUpdateService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 813e7966..2a9dda9a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1015,6 +1015,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && !isEndlessJob ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportRewards[job.rewards]) { rewardManifests.push(job.rewards); rotations.push(ExportRewards[job.rewards].length - 1); @@ -1076,6 +1077,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn); rewardManifests.forEach(name => { const table = ExportRewards[name]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!table) { logger.error(`unknown droptable: ${name}`); return; From 3165d9f459f586dcb23a778bc897e03d71b06e50 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 10:47:38 -0700 Subject: [PATCH 485/776] fix: respect rewardTier for rescue missions (#1650) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1650 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 25 +++++++++++++------ src/types/requestTypes.ts | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2a9dda9a..87a17190 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -55,7 +55,22 @@ import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getWorldState } from "./worldStateService"; -const getRotations = (rotationCount: number, tierOverride: number | undefined): number[] => { +const getRotations = (rewardInfo: IRewardInfo, tierOverride: number | undefined): number[] => { + // For Spy missions, e.g. 3 vaults cracked = A, B, C + if (rewardInfo.VaultsCracked) { + const rotations: number[] = []; + for (let i = 0; i != rewardInfo.VaultsCracked; ++i) { + rotations.push(i); + } + return rotations; + } + + // For Rescue missions + if (rewardInfo.rewardTier) { + return [rewardInfo.rewardTier]; + } + + const rotationCount = rewardInfo.rewardQualifications?.length || 0; if (rotationCount === 0) return [0]; const rotationPattern = @@ -1062,14 +1077,8 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u } else { logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`); } - } else if (RewardInfo.VaultsCracked) { - // For Spy missions, e.g. 3 vaults cracked = A, B, C - for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { - rotations.push(i); - } } else { - const rotationCount = RewardInfo.rewardQualifications?.length || 0; - rotations = getRotations(rotationCount, tierOverride); + rotations = getRotations(RewardInfo, tierOverride); } if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index fff164a8..77e7f322 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -130,7 +130,7 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; VaultsCracked?: number; // for Spy missions - rewardTier?: number; + rewardTier?: number; // for Rescue missions nightmareMode?: boolean; useVaultManifest?: boolean; EnemyCachesFound?: number; From a10c3b061a47da2cbcfe68da7ac7b3043f442554 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:58:15 -0700 Subject: [PATCH 486/776] fix: respect VaultsCracked when rolling droptable for level key rewards (#1639) Fixes #1638 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1639 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 87a17190..2f78f65a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -55,7 +55,7 @@ import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getWorldState } from "./worldStateService"; -const getRotations = (rewardInfo: IRewardInfo, tierOverride: number | undefined): number[] => { +const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C if (rewardInfo.VaultsCracked) { const rotations: number[] = []; @@ -587,7 +587,7 @@ export const addMissionRewards = async ( const fixedLevelRewards = getLevelKeyRewards(levelKeyName); //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); if (fixedLevelRewards.levelKeyRewards) { - addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards); + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo); } if (fixedLevelRewards.levelKeyRewards2) { for (const reward of fixedLevelRewards.levelKeyRewards2) { @@ -627,7 +627,7 @@ export const addMissionRewards = async ( } if (node.missionReward) { - missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards); + missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); } } @@ -870,7 +870,8 @@ export const addCredits = ( export const addFixedLevelRewards = ( rewards: IMissionRewardExternal, inventory: TInventoryDatabaseDocument, - MissionRewards: IMissionReward[] + MissionRewards: IMissionReward[], + rewardInfo?: IRewardInfo ): number => { let missionBonusCredits = 0; if (rewards.credits) { @@ -900,13 +901,16 @@ export const addFixedLevelRewards = ( } if (rewards.droptable) { if (rewards.droptable in ExportRewards) { - logger.debug(`rolling ${rewards.droptable} for level key rewards`); - const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][0]); - if (reward) { - MissionRewards.push({ - StoreItem: reward.type, - ItemCount: reward.itemCount - }); + const rotations: number[] = rewardInfo ? getRotations(rewardInfo) : [0]; + logger.debug(`rolling ${rewards.droptable} for level key rewards`, { rotations }); + for (const tier of rotations) { + const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][tier]); + if (reward) { + MissionRewards.push({ + StoreItem: reward.type, + ItemCount: reward.itemCount + }); + } } } else { logger.error(`unknown droptable ${rewards.droptable}`); From ea0ca8c88bc852e808402dbd6702d1b60c1c0e16 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:35:28 +0200 Subject: [PATCH 487/776] chore: fix file name for giveQuestKeyRewardController --- .../api/{giveQuestKey.ts => giveQuestKeyRewardController.ts} | 0 src/routes/api.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/controllers/api/{giveQuestKey.ts => giveQuestKeyRewardController.ts} (100%) diff --git a/src/controllers/api/giveQuestKey.ts b/src/controllers/api/giveQuestKeyRewardController.ts similarity index 100% rename from src/controllers/api/giveQuestKey.ts rename to src/controllers/api/giveQuestKeyRewardController.ts diff --git a/src/routes/api.ts b/src/routes/api.ts index 1c69da55..39b3d751 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -61,7 +61,7 @@ import { giftingController } from "@/src/controllers/api/giftingController"; import { gildWeaponController } from "@/src/controllers/api/gildWeaponController"; import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; -import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey"; +import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKeyRewardController"; import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController"; import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; import { guildTechController } from "@/src/controllers/api/guildTechController"; From 7a53363b1b3a458a715dde4452dae53c738835b2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:36:46 +0200 Subject: [PATCH 488/776] fix response of giveQuestKeyReward --- src/controllers/api/giveQuestKeyRewardController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/giveQuestKeyRewardController.ts b/src/controllers/api/giveQuestKeyRewardController.ts index 80846234..d74d56bf 100644 --- a/src/controllers/api/giveQuestKeyRewardController.ts +++ b/src/controllers/api/giveQuestKeyRewardController.ts @@ -16,7 +16,7 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) => const inventory = await getInventory(accountId); const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); await inventory.save(); - res.json(inventoryChanges.InventoryChanges); + res.json(inventoryChanges); //TODO: consider whishlist changes }; From 47551e93b3fb9e74c4671e4915e2346acee21d7f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 03:11:21 +0200 Subject: [PATCH 489/776] feat(webui): add everything in ExportCustoms as an "add items" option --- src/controllers/custom/getItemListsController.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 63bf949f..38e42bdb 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService" import { ExportArcanes, ExportAvionics, + ExportCustoms, ExportDrones, ExportGear, ExportKeys, @@ -171,6 +172,12 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(item.name, lang) }); } + for (const [uniqueName, item] of Object.entries(ExportCustoms)) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } res.mods = []; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { From 64fbdf60643ac2306b6ffd78d904668eb2cdd0b3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:11:13 -0700 Subject: [PATCH 490/776] fix: put house version railjack components into the salvage array (#1654) Fixes #1645 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1654 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/inventoryService.ts | 26 +++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a858a5d0..e84d1c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.54", + "warframe-public-export-plus": "^0.5.55", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.54", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.54.tgz", - "integrity": "sha512-27r6qLErr3P8UVDiEzhDAs/BjdAS3vI2CQ58jSI+LClDlj6QL+y1jQe8va/npl3Ft2K8PywLkZ8Yso0j9YzvOA==" + "version": "0.5.55", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.55.tgz", + "integrity": "sha512-Gnd4FCBVuxm2xWGfu8xxxqPIPSnnTqiEWlpP3rsFpVlQs09RNFnW2PEX9rCZt0f3SvHBv5ssDDrFlzgBHS1yrA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 008d5a18..58b231d3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.54", + "warframe-public-export-plus": "^0.5.55", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 48657e88..fe35cff6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -408,8 +408,32 @@ export const addItem = async ( const meta = ExportCustoms[typeName]; let inventoryChanges: IInventoryChanges; if (meta.productCategory == "CrewShipWeaponSkins") { - inventoryChanges = addCrewShipWeaponSkin(inventory, typeName); + if (meta.subroutines || meta.randomisedUpgrades) { + // House versions need to be identified to get stats so put them into raw salvage first. + const rawSalvageChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addCrewShipRawSalvage(inventory, rawSalvageChanges); + inventoryChanges = { CrewShipRawSalvage: rawSalvageChanges }; + } else { + // Sigma versions can be added directly. + if (quantity != 1) { + throw new Error( + `unexpected acquisition quantity of CrewShipWeaponSkin: got ${quantity}, expected 1` + ); + } + inventoryChanges = { + ...addCrewShipWeaponSkin(inventory, typeName), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) + }; + } } else { + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of WeaponSkins: got ${quantity}, expected 1`); + } inventoryChanges = addSkin(inventory, typeName); } if (meta.additionalItems) { From eb6b1c1f57815939771425d50249c78767c8bac0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 03:40:21 +0200 Subject: [PATCH 491/776] chore: fix typo --- src/models/inventoryModels/inventoryModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 9c737d24..6b4b4959 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1613,7 +1613,7 @@ export type InventoryDocumentProps = { QuestKeys: Types.DocumentArray; Drones: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; - CrewShipSalvagedWeaponsSkins: Types.DocumentArray; + CrewShipSalvagedWeaponSkins: Types.DocumentArray; PersonalTechProjects: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; From c13615c4df0fb13346b0a183463bbbf96b2617fe Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:41:46 -0700 Subject: [PATCH 492/776] fix: provide upcoming bounties in worldState when new cycle is imminent (#1657) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1657 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 471 +++++++++++++----- src/types/worldStateTypes.ts | 5 +- .../worldState/worldState.json | 242 --------- 3 files changed, 344 insertions(+), 374 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index dbe61652..15576cef 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -9,7 +9,6 @@ import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; -import { logger } from "../utils/logger"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -80,73 +79,76 @@ const sortieBossNode: Record = { SORTIE_BOSS_INFALAD: "SolNode705" }; -const jobSets: string[][] = [ - [ - "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", - "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt", - "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap", - "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft", - "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", - "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne", - "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", - "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab", - "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc" - ], - [ - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt", - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft", - "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" - ], - [ - "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy" - ], - [ - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate", - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense", - "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation" - ], - [ - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty" - ], - [ - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", - "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" - ] +const eidolonJobs = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", + "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc" +]; + +const eidolonNarmerJobs = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" +]; + +const venusJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy" +]; + +const venusNarmerJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation" +]; + +const microplanetJobs = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty" +]; + +const microplanetEndlessJobs = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" ]; const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 @@ -218,6 +220,10 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; +const expiresBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { + return Date.now() + 300_000 > date; +}; + export const getWorldState = (buildLabel?: string): IWorldState => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc(day / 7); @@ -228,9 +234,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), Goals: [], - GlobalUpgrades: [], + Alerts: [], Sorties: [], LiteSorties: [], + GlobalUpgrades: [], EndlessXpChoices: [], SeasonInfo: { Activation: { $date: { $numberLong: "1715796000000" } }, @@ -268,7 +275,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } ] }, - ...staticWorldState + ...staticWorldState, + SyndicateMissions: [...staticWorldState.SyndicateMissions] }; if (config.worldState?.starDays) { @@ -292,69 +300,272 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - const bountyCycle = Math.trunc(Date.now() / 9000000); - const bountyCycleStart = bountyCycle * 9000000; - const bountyCycleEnd = bountyCycleStart + 9000000; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "ZarimanSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "EntratiLabSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "HexSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - for (const syndicateInfo of worldState.SyndicateMissions) { - if (syndicateInfo.Jobs && syndicateInfo.Seed != bountyCycle) { - syndicateInfo.Activation.$date.$numberLong = bountyCycleStart.toString(10); - syndicateInfo.Expiry.$date.$numberLong = bountyCycleEnd.toString(10); - syndicateInfo.Seed = bountyCycle; - logger.debug(`refreshing jobs for ${syndicateInfo.Tag}`); + let bountyCycle = Math.trunc(Date.now() / 9000000); + let bountyCycleEnd: number | undefined; + do { + const bountyCycleStart = bountyCycle * 9000000; + bountyCycleEnd = bountyCycleStart + 9000000; + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "ZarimanSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "EntratiLabSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "HexSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + + const table = String.fromCharCode(65 + (bountyCycle % 3)); + const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); + const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); + + // TODO: xpAmounts need to be calculated based on the jobType somehow? + + { const rng = new CRng(bountyCycle); - const table = String.fromCharCode(65 + (bountyCycle % 3)); - const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); - const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); - //console.log({ bountyCycleStart, bountyCycleEnd, table, vaultTable, deimosDTable }); - for (const jobInfo of syndicateInfo.Jobs) { - if (jobInfo.jobType) { - let found = false; - for (const jobSet of jobSets) { - if (jobSet.indexOf(jobInfo.jobType) != -1) { - jobInfo.jobType = rng.randomElement(jobSet); - // TODO: xpAmounts seems like it might need to differ depending on the job? - found = true; - break; - } + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000008" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "CetusSyndicate", + Seed: bountyCycle, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [430, 430, 430] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [620, 620, 620] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [670, 670, 670, 990] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [570, 570, 570, 570, 1110] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [740, 740, 740, 740, 1450] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(eidolonNarmerJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [840, 840, 840, 840, 1650] } - if (!found) { - logger.warn(`no job set found for type ${jobInfo.jobType}`); - } - } - if (jobInfo.endless || jobInfo.isVault) { - jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${vaultTable}Rewards`); - } else if (jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierD")) { - jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${deimosDTable}Rewards`); - } else if (!jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierE")) { - jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${table}Rewards`); - } - } + ] + }); } - } + + { + const rng = new CRng(bountyCycle); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000025" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "SolarisSyndicate", + Seed: bountyCycle, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [340, 340, 340] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [660, 660, 660] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [610, 610, 610, 900] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [600, 600, 600, 600, 1170] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [690, 690, 690, 690, 1350] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(venusNarmerJobs), + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [780, 780, 780, 780, 1540] + } + ] + }); + } + + { + const rng = new CRng(bountyCycle); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000002" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "EntratiSyndicate", + Seed: bountyCycle, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [5, 5, 5] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [12, 12, 12] + }, + { + jobType: rng.randomElement(microplanetEndlessJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 25, + maxEnemyLevel: 30, + endless: true, + xpAmounts: [14, 14, 14] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`, + masteryReq: 2, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [17, 17, 17, 25] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [22, 22, 22, 22, 43] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [25, 25, 25, 25, 50] + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [2, 2, 2, 4], + locationTag: "ChamberB", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [4, 4, 4, 5], + locationTag: "ChamberA", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 50, + maxEnemyLevel: 60, + xpAmounts: [5, 5, 5, 7], + locationTag: "ChamberC", + isVault: true + } + ] + }); + } + } while (expiresBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index a3b6efac..aaa292e4 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -5,10 +5,11 @@ export interface IWorldState { BuildLabel: string; Time: number; Goals: IGoal[]; - SyndicateMissions: ISyndicateMissionInfo[]; - GlobalUpgrades: IGlobalUpgrade[]; + Alerts: []; Sorties: ISortie[]; LiteSorties: ILiteSortie[]; + SyndicateMissions: ISyndicateMissionInfo[]; + GlobalUpgrades: IGlobalUpgrade[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; SeasonInfo: { diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index d9e228ae..3b4143e5 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -278,248 +278,6 @@ "Tag": "SteelMeridianSyndicate", "Seed": 42366, "Nodes": ["SolNode27", "SolNode107", "SolNode214", "SettlementNode1", "SolNode177", "SolNode141", "SolNode408"] - }, - { - "_id": { "$oid": "663a71c80000000000000002" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "EntratiSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [5, 5, 5] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 15, - "maxEnemyLevel": 25, - "xpAmounts": [12, 12, 12] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards", - "masteryReq": 5, - "minEnemyLevel": 25, - "maxEnemyLevel": 30, - "endless": true, - "xpAmounts": [14, 14, 14] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 30, - "maxEnemyLevel": 40, - "xpAmounts": [17, 17, 17, 25] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", - "masteryReq": 3, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [22, 22, 22, 22, 43] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [25, 25, 25, 25, 50] - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATableCRewards", - "masteryReq": 5, - "minEnemyLevel": 30, - "maxEnemyLevel": 40, - "xpAmounts": [2, 2, 2, 4], - "locationTag": "ChamberB", - "isVault": true - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTableCRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 50, - "xpAmounts": [4, 4, 4, 5], - "locationTag": "ChamberA", - "isVault": true - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards", - "masteryReq": 5, - "minEnemyLevel": 50, - "maxEnemyLevel": 60, - "xpAmounts": [5, 5, 5, 7], - "locationTag": "ChamberC", - "isVault": true - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000004" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "EntratiLabSyndicate", - "Seed": 99562, - "Nodes": [] - }, - { - "_id": { "$oid": "663a71c80000000000000008" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "CetusSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [430, 430, 430] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 10, - "maxEnemyLevel": 30, - "xpAmounts": [620, 620, 620] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 20, - "maxEnemyLevel": 40, - "xpAmounts": [670, 670, 670, 990] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTableBRewards", - "masteryReq": 3, - "minEnemyLevel": 30, - "maxEnemyLevel": 50, - "xpAmounts": [570, 570, 570, 570, 1110] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [740, 740, 740, 740, 1450] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [840, 840, 840, 840, 1660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - "masteryReq": 0, - "minEnemyLevel": 50, - "maxEnemyLevel": 70, - "xpAmounts": [840, 840, 840, 840, 1650] - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000025" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "SolarisSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [340, 340, 340] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 10, - "maxEnemyLevel": 30, - "xpAmounts": [660, 660, 660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 20, - "maxEnemyLevel": 40, - "xpAmounts": [610, 610, 610, 900] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTableBRewards", - "masteryReq": 3, - "minEnemyLevel": 30, - "maxEnemyLevel": 50, - "xpAmounts": [600, 600, 600, 600, 1170] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [690, 690, 690, 690, 1350] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [840, 840, 840, 840, 1660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - "masteryReq": 0, - "minEnemyLevel": 50, - "maxEnemyLevel": 70, - "xpAmounts": [780, 780, 780, 780, 1540] - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000029" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "ZarimanSyndicate", - "Seed": 99562, - "Nodes": [] - }, - { - "_id": { "$oid": "676b8d340000000000000006" }, - "Activation": { "$date": { "$numberLong": "1735101748215" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "HexSyndicate", - "Seed": 33872, - "Nodes": [] } ], "ActiveMissions": [ From 95562a97ad617505b2df50b608438e898445b087 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:53:27 -0700 Subject: [PATCH 493/776] fix: provide current & upcoming sortie if rollover is imminent (#1666) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1666 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 276 +++++++++++++++--------------- 1 file changed, 136 insertions(+), 140 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 15576cef..5137b5ed 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -8,7 +8,7 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; +import { ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -153,6 +153,10 @@ const microplanetEndlessJobs = [ const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 +const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { + return Date.now() + 300_000 > date; +}; + const getSortieTime = (day: number): number => { const dayStart = EPOCH + day * 86400000; const date = new Date(dayStart); @@ -167,6 +171,134 @@ const getSortieTime = (day: number): number => { return dayStart + (isDst ? 16 : 17) * 3600000; }; +const pushSortieIfRelevant = (out: ISortie[], day: number): void => { + const dayStart = getSortieTime(day); + if (!isBeforeNextExpectedWorldStateRefresh(dayStart)) { + return; + } + const dayEnd = getSortieTime(day + 1); + if (Date.now() >= dayEnd) { + return; + } + + const rng = new CRng(day); + + const boss = rng.randomElement(sortieBosses); + + const modifiers = [ + "SORTIE_MODIFIER_LOW_ENERGY", + "SORTIE_MODIFIER_IMPACT", + "SORTIE_MODIFIER_SLASH", + "SORTIE_MODIFIER_PUNCTURE", + "SORTIE_MODIFIER_EXIMUS", + "SORTIE_MODIFIER_MAGNETIC", + "SORTIE_MODIFIER_CORROSIVE", + "SORTIE_MODIFIER_VIRAL", + "SORTIE_MODIFIER_ELECTRICITY", + "SORTIE_MODIFIER_RADIATION", + "SORTIE_MODIFIER_GAS", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_TOXIN", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_HAZARD_RADIATION", + "SORTIE_MODIFIER_HAZARD_MAGNETIC", + "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest + "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon + "SORTIE_MODIFIER_HAZARD_ICE", + "SORTIE_MODIFIER_HAZARD_COLD", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_MELEE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + + if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); + if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); + + const nodes: string[] = []; + const availableMissionIndexes: number[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && + sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 5 && // Exclude MT_CAPTURE + value.missionIndex != 21 && // Exclude MT_PURIFY + value.missionIndex != 23 && // Exclude MT_JUNCTION + value.missionIndex <= 28 + ) { + if (!availableMissionIndexes.includes(value.missionIndex)) { + availableMissionIndexes.push(value.missionIndex); + } + nodes.push(key); + } + } + + const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; + const missionTypes = new Set(); + + for (let i = 0; i < 3; i++) { + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; + let missionIndex = ExportRegions[node].missionIndex; + + if ( + !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions + missionIndex != 28 && + rng.randomInt(0, 2) == 2 + ) { + missionIndex = rng.randomElement(availableMissionIndexes); + } + + if (i == 2 && rng.randomInt(0, 2) == 2) { + const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); + const modifierType = rng.randomElement(filteredModifiers); + + if (boss == "SORTIE_BOSS_PHORID") { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); + nodes.splice(randomIndex, 1); + continue; + } else if (sortieBossNode[boss]) { + selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); + continue; + } + } + + const missionType = eMissionType[missionIndex].tag; + + if (missionTypes.has(missionType)) { + i--; + continue; + } + + const filteredModifiers = + missionType === "MT_TERRITORY" + ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") + : modifiers; + + const modifierType = rng.randomElement(filteredModifiers); + + selectedNodes.push({ missionType, modifierType, node }); + nodes.splice(randomIndex, 1); + missionTypes.add(missionType); + } + + out.push({ + _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", + Seed: day, + Boss: boss, + Variants: selectedNodes + }); +}; + const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") ); @@ -220,10 +352,6 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; -const expiresBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { - return Date.now() + 300_000 > date; -}; - export const getWorldState = (buildLabel?: string): IWorldState => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc(day / 7); @@ -565,7 +693,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { ] }); } - } while (expiresBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); + } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ @@ -605,140 +733,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Sortie cycling every day - { - let genDay; - let dayStart; - let dayEnd; - const sortieRolloverToday = getSortieTime(day); - if (Date.now() < sortieRolloverToday) { - // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`. - genDay = day - 1; - dayStart = getSortieTime(genDay); - dayEnd = sortieRolloverToday; - } else { - // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`. - genDay = day; - dayStart = sortieRolloverToday; - dayEnd = getSortieTime(day + 1); - } - - const rng = new CRng(genDay); - - const boss = rng.randomElement(sortieBosses); - - const modifiers = [ - "SORTIE_MODIFIER_LOW_ENERGY", - "SORTIE_MODIFIER_IMPACT", - "SORTIE_MODIFIER_SLASH", - "SORTIE_MODIFIER_PUNCTURE", - "SORTIE_MODIFIER_EXIMUS", - "SORTIE_MODIFIER_MAGNETIC", - "SORTIE_MODIFIER_CORROSIVE", - "SORTIE_MODIFIER_VIRAL", - "SORTIE_MODIFIER_ELECTRICITY", - "SORTIE_MODIFIER_RADIATION", - "SORTIE_MODIFIER_GAS", - "SORTIE_MODIFIER_FIRE", - "SORTIE_MODIFIER_EXPLOSION", - "SORTIE_MODIFIER_FREEZE", - "SORTIE_MODIFIER_TOXIN", - "SORTIE_MODIFIER_POISON", - "SORTIE_MODIFIER_HAZARD_RADIATION", - "SORTIE_MODIFIER_HAZARD_MAGNETIC", - "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest - "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon - "SORTIE_MODIFIER_HAZARD_ICE", - "SORTIE_MODIFIER_HAZARD_COLD", - "SORTIE_MODIFIER_SECONDARY_ONLY", - "SORTIE_MODIFIER_SHOTGUN_ONLY", - "SORTIE_MODIFIER_SNIPER_ONLY", - "SORTIE_MODIFIER_RIFLE_ONLY", - "SORTIE_MODIFIER_MELEE_ONLY", - "SORTIE_MODIFIER_BOW_ONLY" - ]; - - if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); - if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); - - const nodes: string[] = []; - const availableMissionIndexes: number[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && - sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && - value.name.indexOf("Archwing") == -1 && - value.missionIndex != 0 && // Exclude MT_ASSASSINATION - value.missionIndex != 5 && // Exclude MT_CAPTURE - value.missionIndex != 21 && // Exclude MT_PURIFY - value.missionIndex != 23 && // Exclude MT_JUNCTION - value.missionIndex <= 28 - ) { - if (!availableMissionIndexes.includes(value.missionIndex)) { - availableMissionIndexes.push(value.missionIndex); - } - nodes.push(key); - } - } - - const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; - const missionTypes = new Set(); - - for (let i = 0; i < 3; i++) { - const randomIndex = rng.randomInt(0, nodes.length - 1); - const node = nodes[randomIndex]; - let missionIndex = ExportRegions[node].missionIndex; - - if ( - !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions - missionIndex != 28 && - rng.randomInt(0, 2) == 2 - ) { - missionIndex = rng.randomElement(availableMissionIndexes); - } - - if (i == 2 && rng.randomInt(0, 2) == 2) { - const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); - const modifierType = rng.randomElement(filteredModifiers); - - if (boss == "SORTIE_BOSS_PHORID") { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); - nodes.splice(randomIndex, 1); - continue; - } else if (sortieBossNode[boss]) { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); - continue; - } - } - - const missionType = eMissionType[missionIndex].tag; - - if (missionTypes.has(missionType)) { - i--; - continue; - } - - const filteredModifiers = - missionType === "MT_TERRITORY" - ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") - : modifiers; - - const modifierType = rng.randomElement(filteredModifiers); - - selectedNodes.push({ missionType, modifierType, node }); - nodes.splice(randomIndex, 1); - missionTypes.add(missionType); - } - - worldState.Sorties.push({ - _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, - Activation: { $date: { $numberLong: dayStart.toString() } }, - Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", - Seed: genDay, - Boss: boss, - Variants: selectedNodes - }); - } + pushSortieIfRelevant(worldState.Sorties, day - 1); + pushSortieIfRelevant(worldState.Sorties, day); // Archon Hunt cycling every week { From a738dbfa9a64726b6928287ac167c33e845d25bd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:28:34 -0700 Subject: [PATCH 494/776] fix: use JobTier instead of parsing the jobId for it (#1649) Should fix #1647 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1649 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2f78f65a..c932ac0b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -738,14 +738,12 @@ export const addMissionRewards = async ( if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [jobType, tierStr, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_"); - const tier = Number(tierStr); - + const [jobType, unkIndex, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_"); const worldState = getWorldState(); let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId); if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag if (syndicateEntry && syndicateEntry.Jobs) { - let currentJob = syndicateEntry.Jobs[tier]; + let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); if (vault) currentJob = vault; @@ -770,7 +768,7 @@ export const addMissionRewards = async ( }); SyndicateXPItemReward = medallionAmount; } else { - if (tier >= 0) { + if (rewardInfo.JobTier! >= 0) { AffiliationMods.push( addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage]) ); @@ -946,8 +944,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (RewardInfo.jobId) { if (RewardInfo.JobStage! >= 0) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [jobType, tierStr, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); - const tier = Number(tierStr); + const [jobType, unkIndex, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); let isEndlessJob = false; if (syndicateId) { const worldState = getWorldState(); @@ -955,7 +952,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); if (syndicateEntry && syndicateEntry.Jobs) { - let job = syndicateEntry.Jobs[tier]; + let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); From 38502f10bf30c2ee1b04af4d86f115809c6ff83d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:28:52 -0700 Subject: [PATCH 495/776] fix: give ample duplicates of ship decos with unlockAllShipDecorations (#1651) Closes #1644 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1651 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index bf71ae42..910420cf 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -149,7 +149,7 @@ export const getInventoryResponse = async ( inventoryResponse.ShipDecorations = []; for (const [uniqueName, item] of Object.entries(ExportResources)) { if (item.productCategory == "ShipDecorations") { - inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 }); + inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 }); } } } From 729061951fb1e54632e0c457b32db412f7596b86 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:29:09 -0700 Subject: [PATCH 496/776] fix: allow manageQuests' deleteKey op to be used with any ItemType (#1653) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1653 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 | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 49ae004c..04650a06 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -56,15 +56,12 @@ export const manageQuestsController: RequestHandler = async (req, res) => { break; } case "deleteKey": { - if (allQuestKeys.includes(questItemType)) { - const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); - if (!questKey) { - logger.error(`Quest key not found in inventory: ${questItemType}`); - break; - } - - inventory.QuestKeys.pull({ ItemType: questItemType }); + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; } + inventory.QuestKeys.pull({ ItemType: questItemType }); break; } case "completeKey": { From 4cb1ea94e5c2e30151dbd6cc5458f53f82a8dd7e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:29:27 -0700 Subject: [PATCH 497/776] feat: sell/scrap CrewShipWeapons (#1655) Closes #1646 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1655 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/sellController.ts | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index ad31c259..f6840589 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -6,9 +6,12 @@ import { addRecipes, addMiscItems, addConsumables, - freeUpSlot + freeUpSlot, + combineInventoryChanges } from "@/src/services/inventoryService"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const sellController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as ISellRequest; @@ -48,6 +51,9 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.Hoverboards) { requiredFields.add(InventorySlot.SPACESUITS); } + if (payload.Items.CrewShipWeapons) { + requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); // Give currency @@ -69,10 +75,14 @@ export const sellController: RequestHandler = async (req, res) => { ItemCount: payload.SellPrice } ]); + } else if (payload.SellCurrency == "SC_Resources") { + // Will add appropriate MiscItems from CrewShipWeapons } else { throw new Error("Unknown SellCurrency: " + payload.SellCurrency); } + const inventoryChanges: IInventoryChanges = {}; + // Remove item(s) if (payload.Items.Suits) { payload.Items.Suits.forEach(sellItem => { @@ -145,6 +155,24 @@ export const sellController: RequestHandler = async (req, res) => { inventory.Drones.pull({ _id: sellItem.String }); }); } + if (payload.Items.CrewShipWeapons) { + payload.Items.CrewShipWeapons.forEach(sellItem => { + const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String)); + if (index != -1) { + const itemType = inventory.CrewShipWeapons[index].ItemType; + const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType)!; + const miscItemChanges = recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: Math.trunc(x.ItemCount * 0.8) + })); + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + + inventory.CrewShipWeapons.splice(index, 1); + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } + }); + } if (payload.Items.Consumables) { const consumablesChanges = []; for (const sellItem of payload.Items.Consumables) { @@ -191,7 +219,9 @@ export const sellController: RequestHandler = async (req, res) => { } await inventory.save(); - res.json({}); + res.json({ + inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" + }); }; interface ISellRequest { @@ -212,6 +242,7 @@ interface ISellRequest { OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; Drones?: ISellItem[]; + CrewShipWeapons?: ISellItem[]; }; SellPrice: number; SellCurrency: From 7d607b7348216ea75133eace136779d13a4b73b2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:29:50 -0700 Subject: [PATCH 498/776] fix: check ascension ceremony contributors when changing clan tier (#1656) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1656 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/contributeGuildClassController.ts | 43 ++---------------- src/services/guildService.ts | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index 1a20f4ef..c4fa7280 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -1,8 +1,7 @@ import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { Guild, GuildMember } from "@/src/models/guildModel"; -import { config } from "@/src/services/configService"; -import { createMessage } from "@/src/services/inboxService"; +import { Guild } from "@/src/models/guildModel"; +import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; @@ -31,43 +30,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) = guild.CeremonyContributors.push(new Types.ObjectId(accountId)); - // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. - if (guild.CeremonyContributors.length == payload.RequiredContributors) { - guild.Class = guild.CeremonyClass!; - guild.CeremonyClass = undefined; - guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); - if (!config.fastClanAscension) { - // Send message to all active guild members - const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId"); - for (const member of members) { - // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". - await createMessage(member.accountId, [ - { - sndr: guild.Name, - msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", - arg: [ - { - Key: "RESETDATE", - Tag: - guild.CeremonyResetDate.getUTCMonth() + - "/" + - guild.CeremonyResetDate.getUTCDate() + - "/" + - (guild.CeremonyResetDate.getUTCFullYear() % 100) + - " " + - guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") + - ":" + - guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0") - } - ], - sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", - icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", - highPriority: true - } - ]); - } - } - } + await checkClanAscensionHasRequiredContributors(guild); await guild.save(); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 15b973f8..e490c304 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -31,6 +31,7 @@ import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTy import { IInventoryChanges } from "../types/purchaseTypes"; import { parallelForeach } from "../utils/async-utils"; import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json"; +import { createMessage } from "./inboxService"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -601,6 +602,50 @@ const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Pro await processGuildTechProjectContributionsUpdate(guild, project); } } + if (guild.CeremonyContributors) { + await checkClanAscensionHasRequiredContributors(guild); + } +}; + +export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDatabaseDocument): Promise => { + const requiredContributors = [1, 5, 15, 30, 50][guild.Tier - 1]; + // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. + if (guild.CeremonyContributors!.length >= requiredContributors) { + guild.Class = guild.CeremonyClass!; + guild.CeremonyClass = undefined; + guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); + if (!config.fastClanAscension) { + // Send message to all active guild members + const members = await GuildMember.find({ guildId: guild._id, status: 0 }, "accountId"); + await parallelForeach(members, async member => { + // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". + await createMessage(member.accountId, [ + { + sndr: guild.Name, + msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", + arg: [ + { + Key: "RESETDATE", + Tag: + guild.CeremonyResetDate!.getUTCMonth() + + "/" + + guild.CeremonyResetDate!.getUTCDate() + + "/" + + (guild.CeremonyResetDate!.getUTCFullYear() % 100) + + " " + + guild.CeremonyResetDate!.getUTCHours().toString().padStart(2, "0") + + ":" + + guild.CeremonyResetDate!.getUTCMinutes().toString().padStart(2, "0") + } + ], + sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", + icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", + highPriority: true + } + ]); + }); + } + } }; export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => { From 46aef2c00e668b71e1a54d09db803e9d56ca0577 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:30:06 -0700 Subject: [PATCH 499/776] feat: send jordas precept email when completing pluto to eris junction (#1660) Closes #1659 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1660 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c932ac0b..d7872fc8 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -629,6 +629,19 @@ export const addMissionRewards = async ( if (node.missionReward) { missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); } + + if (missions.Tag == "PlutoToErisJunction") { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/G1Quests/GolemQuestJordasName", + msg: "/Lotus/Language/G1Quests/GolemQuestIntroBody", + att: ["/Lotus/Types/Keys/GolemQuest/GolemQuestKeyChainItem"], + sub: "/Lotus/Language/G1Quests/GolemQuestIntroTitle", + icon: "/Lotus/Interface/Icons/Npcs/JordasPortrait.png", + highPriority: true + } + ]); + } } if (rewardInfo.useVaultManifest) { From 3d1b009bdb8dab1f34cbdefb6d6d55c13bd7ecec Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:30:22 -0700 Subject: [PATCH 500/776] feat: noDailyFocusLimit cheat (#1661) Closes #1641 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1661 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + src/controllers/api/inventoryController.ts | 4 ++++ src/services/configService.ts | 1 + src/services/inventoryService.ts | 4 +++- static/webui/index.html | 4 ++++ 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/zh.js | 1 + 11 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 907f8af7..9d072329 100644 --- a/config.json.example +++ b/config.json.example @@ -29,6 +29,7 @@ "unlockExilusEverywhere": false, "unlockArcanesEverywhere": false, "noDailyStandingLimits": false, + "noDailyFocusLimit": false, "noArgonCrystalDecay": false, "noMasteryRankUpCooldown": false, "noVendorPurchaseLimits": true, diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 910420cf..bede097f 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -258,6 +258,10 @@ export const getInventoryResponse = async ( } } + if (config.noDailyFocusLimit) { + inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000); + } + if (inventoryResponse.InfestedFoundry) { applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); } diff --git a/src/services/configService.ts b/src/services/configService.ts index 88a6634a..cb1a2c38 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -35,6 +35,7 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noDailyFocusLimit?: boolean; noArgonCrystalDecay?: boolean; noMasteryRankUpCooldown?: boolean; noVendorPurchaseLimits?: boolean; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index fe35cff6..2d17f7ed 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1372,7 +1372,9 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; - inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); + if (!config.noDailyFocusLimit) { + inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); + } }; export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => { diff --git a/static/webui/index.html b/static/webui/index.html index d4a900c3..ec786b4e 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -595,6 +595,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 7cca988a..0e69b371 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 01a97404..bbf2561a 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -133,6 +133,7 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_noDailyFocusLimit: `No Daily Focus Limit`, cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 2c303d80..e2ea40ec 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`, cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`, cheats_noDailyStandingLimits: `Sin límite diario de reputación`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index e2ebd5e7..54f041c6 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 1def5df7..62c82f70 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 1ee2ea4c..3d7af8ec 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -134,6 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `全物品自带适配器`, cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, cheats_noDailyStandingLimits: `无每日声望限制`, + cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, From 51b82df5fd2e7f324b7cfb3f3531545daf7ba2ad Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:30:36 -0700 Subject: [PATCH 501/776] feat: granum void/purgatory rewards (#1663) Closes #1627 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1663 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index d7872fc8..21298951 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1141,6 +1141,32 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } } + + if (RewardInfo.PurgatoryRewardQualifications) { + for (const encodedQualification of RewardInfo.PurgatoryRewardQualifications) { + const qualification = parseInt(encodedQualification) - 1; + if (qualification < 0 || qualification > 8) { + logger.error(`unexpected purgatory reward qualification: ${qualification}`); + } else { + const drop = getRandomRewardByChance( + ExportRewards[ + [ + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards" + ][Math.trunc(qualification / 3)] + ][qualification % 3] + ); + if (drop) { + drops.push({ + StoreItem: drop.type, + ItemCount: drop.itemCount, + FromEnemyCache: true // to show "identified" + }); + } + } + } + } } return drops; } From 0ea67ea89ade9f48baf499cce2f824c356bc5dad Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:31:00 -0700 Subject: [PATCH 502/776] feat: identify & repair railjack components (#1664) Closes #911 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1664 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 41 +++++++ src/controllers/api/guildTechController.ts | 114 +++++++++++++----- src/models/inventoryModels/inventoryModel.ts | 5 + src/routes/api.ts | 2 + src/services/inventoryService.ts | 24 +++- src/types/inventoryTypes/inventoryTypes.ts | 8 +- src/types/purchaseTypes.ts | 1 + 7 files changed, 158 insertions(+), 37 deletions(-) create mode 100644 src/controllers/api/crewShipIdentifySalvageController.ts diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts new file mode 100644 index 00000000..c40f78c5 --- /dev/null +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -0,0 +1,41 @@ +import { addCrewShipSalvagedWeaponSkin, addCrewShipRawSalvage, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportCustoms } from "warframe-public-export-plus"; +import { IFingerprintStat } from "@/src/helpers/rivenHelper"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; + +export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "CrewShipSalvagedWeaponSkins CrewShipRawSalvage"); + const payload = getJSONfromString(String(req.body)); + + const buffs: IFingerprintStat[] = []; + for (const upgrade of ExportCustoms[payload.ItemType].randomisedUpgrades!) { + buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } + const inventoryChanges: IInventoryChanges = addCrewShipSalvagedWeaponSkin( + inventory, + payload.ItemType, + JSON.stringify({ compat: payload.ItemType, buffs } satisfies IInnateDamageFingerprint) + ); + + inventoryChanges.CrewShipRawSalvage = [ + { + ItemType: payload.ItemType, + ItemCount: -1 + } + ]; + addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage); + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); +}; + +interface ICrewShipIdentifySalvageRequest { + ItemType: string; +} diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 55083d5a..257434fb 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -14,20 +14,23 @@ import { import { ExportDojoRecipes } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { + addCrewShipWeaponSkin, addItem, addMiscItems, addRecipes, combineInventoryChanges, getInventory, + occupySlot, updateCurrency } from "@/src/services/inventoryService"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; import { GuildMember } from "@/src/models/guildModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -99,6 +102,8 @@ export const guildTechController: RequestHandler = async (req, res) => { State: 0, ReqCredits: recipe.price, ItemType: data.RecipeType, + ProductCategory: data.TechProductCategory, + CategoryItemId: data.CategoryItemId, ReqItems: recipe.ingredients }) - 1 ]; @@ -222,33 +227,44 @@ export const guildTechController: RequestHandler = async (req, res) => { }); } } else if (data.Action.split(",")[0] == "Buy") { - const guild = await getGuildForRequestEx(req, inventory); - if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { - res.status(400).send("-1").end(); - return; - } const purchase = data as IGuildTechBuyRequest; - const quantity = parseInt(data.Action.split(",")[1]); - const recipeChanges = [ - { - ItemType: purchase.RecipeType, - ItemCount: quantity + if (purchase.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + if ( + !hasAccessToDojo(inventory) || + !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator)) + ) { + res.status(400).send("-1").end(); + return; } - ]; - addRecipes(inventory, recipeChanges); - const currencyChanges = updateCurrency( - inventory, - ExportDojoRecipes.research[purchase.RecipeType].replicatePrice, - false - ); - await inventory.save(); - // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. - res.json({ - inventoryChanges: { - ...currencyChanges, - Recipes: recipeChanges - } - }); + const quantity = parseInt(data.Action.split(",")[1]); + const recipeChanges = [ + { + ItemType: purchase.RecipeType, + ItemCount: quantity + } + ]; + addRecipes(inventory, recipeChanges); + const currencyChanges = updateCurrency( + inventory, + ExportDojoRecipes.research[purchase.RecipeType].replicatePrice, + false + ); + await inventory.save(); + // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. + res.json({ + inventoryChanges: { + ...currencyChanges, + Recipes: recipeChanges + } + }); + } else { + const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!); + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); + } } else if (data.Action == "Fabricate") { const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { @@ -289,9 +305,18 @@ export const guildTechController: RequestHandler = async (req, res) => { guild.ActiveDojoColorResearch = data.RecipeType; await guild.save(); res.end(); + } else if (data.Action == "Rush" && data.CategoryItemId) { + const inventoryChanges: IInventoryChanges = { + ...updateCurrency(inventory, 20, true), + ...claimSalvagedComponent(inventory, data.CategoryItemId) + }; + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); } else { logger.debug(`data provided to ${req.path}: ${String(req.body)}`); - throw new Error(`unknown guildTech action: ${data.Action}`); + throw new Error(`unhandled guildTech request`); } }; @@ -301,15 +326,15 @@ type TGuildTechRequest = | IGuildTechContributeRequest; interface IGuildTechBasicRequest { - Action: "Start" | "Fabricate" | "Pause" | "Unpause"; + Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush"; Mode: "Guild" | "Personal"; RecipeType: string; + TechProductCategory?: string; + CategoryItemId?: string; } -interface IGuildTechBuyRequest { +interface IGuildTechBuyRequest extends Omit { Action: string; - Mode: "Guild"; - RecipeType: string; } interface IGuildTechContributeRequest { @@ -321,3 +346,30 @@ interface IGuildTechContributeRequest { VaultCredits: number; VaultMiscItems: IMiscItem[]; } + +const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => { + // delete personal tech project + const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId)); + if (personalTechProjectIndex != -1) { + inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); + } + + // find salved part & delete it + const crewShipSalvagedWeaponSkinsIndex = inventory.CrewShipSalvagedWeaponSkins.findIndex(x => x._id.equals(itemId)); + const crewShipWeaponSkin = inventory.CrewShipSalvagedWeaponSkins[crewShipSalvagedWeaponSkinsIndex]; + inventory.CrewShipSalvagedWeaponSkins.splice(crewShipSalvagedWeaponSkinsIndex, 1); + + // add final item + const inventoryChanges = { + ...addCrewShipWeaponSkin(inventory, crewShipWeaponSkin.ItemType, crewShipWeaponSkin.UpgradeFingerprint), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) + }; + + inventoryChanges.RemovedIdItems = [ + { + ItemId: { $oid: itemId } + } + ]; + + return inventoryChanges; +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 6b4b4959..97a0022e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -504,6 +504,8 @@ const personalTechProjectSchema = new Schema({ State: Number, ReqCredits: Number, ItemType: String, + ProductCategory: String, + CategoryItemId: Schema.Types.ObjectId, ReqItems: { type: [typeCountSchema], default: undefined }, HasContributions: Boolean, CompletionDate: Date @@ -522,6 +524,9 @@ personalTechProjectSchema.set("toJSON", { const db = ret as IPersonalTechProjectDatabase; const client = ret as IPersonalTechProjectClient; + if (db.CategoryItemId) { + client.CategoryItemId = toOid(db.CategoryItemId); + } if (db.CompletionDate) { client.CompletionDate = toMongoDate(db.CompletionDate); } diff --git a/src/routes/api.ts b/src/routes/api.ts index 39b3d751..d16ea321 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -27,6 +27,7 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController"; @@ -218,6 +219,7 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2d17f7ed..d3e86bfd 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -426,7 +426,7 @@ export const addItem = async ( ); } inventoryChanges = { - ...addCrewShipWeaponSkin(inventory, typeName), + ...addCrewShipWeaponSkin(inventory, typeName, undefined), ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) }; } @@ -1107,12 +1107,14 @@ export const addSkin = ( return inventoryChanges; }; -const addCrewShipWeaponSkin = ( +export const addCrewShipWeaponSkin = ( inventory: TInventoryDatabaseDocument, typeName: string, + upgradeFingerprint: string | undefined, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1; + const index = + inventory.CrewShipWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.CrewShipWeaponSkins ??= []; (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push( @@ -1121,6 +1123,22 @@ const addCrewShipWeaponSkin = ( return inventoryChanges; }; +export const addCrewShipSalvagedWeaponSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + upgradeFingerprint: string | undefined, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = + inventory.CrewShipSalvagedWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.CrewShipSalvagedWeaponSkins ??= []; + (inventoryChanges.CrewShipSalvagedWeaponSkins as IUpgradeClient[]).push( + inventory.CrewShipSalvagedWeaponSkins[index].toJSON() + ); + return inventoryChanges; +}; + const addCrewShip = ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 63207ef0..73156437 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -947,15 +947,17 @@ export interface IPersonalTechProjectDatabase { State: number; ReqCredits: number; ItemType: string; + ProductCategory?: string; + CategoryItemId?: Types.ObjectId; ReqItems: ITypeCount[]; HasContributions?: boolean; CompletionDate?: Date; } -export interface IPersonalTechProjectClient extends Omit { - CompletionDate?: IMongoDate; - ProductCategory?: string; +export interface IPersonalTechProjectClient + extends Omit { CategoryItemId?: IOid; + CompletionDate?: IMongoDate; ItemId: IOid; } diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 51459454..1b4c9aca 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -43,6 +43,7 @@ export type IInventoryChanges = { Drones?: IDroneClient[]; MiscItems?: IMiscItem[]; EmailItems?: ITypeCount[]; + CrewShipRawSalvage?: ITypeCount[]; Nemesis?: Partial; NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 From deb652ab37a164e6f992fda3d6b1175d54fe79f1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:31:16 -0700 Subject: [PATCH 503/776] fix: provide upcoming nightwave daily challenge if rollover is imminent (#1667) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1667 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 62 +++++++++++++++++-------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 5137b5ed..55f6afd9 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -374,34 +374,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Season: 14, Phase: 0, Params: "", - ActiveChallenges: [ - getSeasonDailyChallenge(day - 2), - getSeasonDailyChallenge(day - 1), - getSeasonDailyChallenge(day - 0), - getSeasonWeeklyChallenge(week, 0), - getSeasonWeeklyChallenge(week, 1), - getSeasonWeeklyHardChallenge(week, 2), - getSeasonWeeklyHardChallenge(week, 3), - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: - "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) - }, - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) - }, - { - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) - } - ] + ActiveChallenges: [] }, ...staticWorldState, SyndicateMissions: [...staticWorldState.SyndicateMissions] @@ -424,6 +397,37 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); } + // Nightwave Challenges + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0)); + if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); + } + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + }); + // TODO: Provide upcoming weekly acts if rollover is imminent + // Elite Sanctuary Onslaught cycling every week worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful @@ -737,6 +741,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { pushSortieIfRelevant(worldState.Sorties, day); // Archon Hunt cycling every week + // TODO: Handle imminent rollover { const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; @@ -829,6 +834,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); // 1999 Calendar Season cycling every week + YearIteration every 4 weeks + // TODO: Handle imminent rollover worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } }; worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } }; worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4]; From 44da0eb50a40e2d05b67123aafaddc1841c7f460 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:35:39 +0200 Subject: [PATCH 504/776] docs: some basic explanation of config.json & config.json.example --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ef091058..e1ef957a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ To get an idea of what functionality you can expect to be missing [have a look t ## config.json +SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled. + - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE. From 379e83a764988ece565f11c677d831e95dcdd9fc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 08:36:00 -0700 Subject: [PATCH 505/776] fix: use rewardTier only for rescue missions (#1674) Fixes #1672 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1674 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 2 +- src/types/requestTypes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 21298951..0b452ccc 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -66,7 +66,7 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] } // For Rescue missions - if (rewardInfo.rewardTier) { + if (rewardInfo.node in ExportRegions && ExportRegions[rewardInfo.node].missionIndex == 3 && rewardInfo.rewardTier) { return [rewardInfo.rewardTier]; } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 77e7f322..fff164a8 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -130,7 +130,7 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; VaultsCracked?: number; // for Spy missions - rewardTier?: number; // for Rescue missions + rewardTier?: number; nightmareMode?: boolean; useVaultManifest?: boolean; EnemyCachesFound?: number; From 9a50c05205bb49f2227332286c054bb9c232a11e Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 16 Apr 2025 09:01:47 -0700 Subject: [PATCH 506/776] chore(webui): update to German translation (#1680) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1680 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 0e69b371..20be9fbd 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -133,8 +133,8 @@ dict = { cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`, cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, - cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, - cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, + cheats_noDailyStandingLimits: `Kein tägliches Ansehen Limit`, + cheats_noDailyFocusLimit: `Kein tägliches Fokus-Limit`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, From 66dae6d3f867ae736014356cec619b4261573464 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Wed, 16 Apr 2025 09:28:40 -0700 Subject: [PATCH 507/776] chore(webui): update Spanish translation (#1681) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1681 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index e2ea40ec..c979bddc 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -134,7 +134,7 @@ dict = { cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`, cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`, cheats_noDailyStandingLimits: `Sin límite diario de reputación`, - cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, + cheats_noDailyFocusLimit: `Límites diarios de enfoque desactivados`, cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, From 850a07359423d8ce5454c31c13fb088b3fda7b9c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:37:40 -0700 Subject: [PATCH 508/776] fix: don't set IsNew on CrewShipWeapons (#1682) this indicator doesn't fully work for them as it seems the client doesn't clear it, so I assume they're not supposed to have it Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1682 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index d3e86bfd..391f40ae 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1066,7 +1066,7 @@ export const addEquipment = ( Configs: [], XP: 0, ModularParts: modularParts, - IsNew: true + IsNew: category != "CrewShipWeapons" ? true : undefined }, defaultOverwrites ); From 16e850e7eeac52d450b14147dad09d13b6a3a613 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:38:27 -0700 Subject: [PATCH 509/776] fix: provide a SubroutineIndex when identifying applicable components (#1683) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1683 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 19 +++++++++++++------ src/types/inventoryTypes/inventoryTypes.ts | 4 ++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts index c40f78c5..d9d09b75 100644 --- a/src/controllers/api/crewShipIdentifySalvageController.ts +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -1,25 +1,32 @@ import { addCrewShipSalvagedWeaponSkin, addCrewShipRawSalvage, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; -import { IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ICrewShipComponentFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; import { ExportCustoms } from "warframe-public-export-plus"; -import { IFingerprintStat } from "@/src/helpers/rivenHelper"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { getRandomInt } from "@/src/services/rngService"; export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "CrewShipSalvagedWeaponSkins CrewShipRawSalvage"); const payload = getJSONfromString(String(req.body)); - const buffs: IFingerprintStat[] = []; - for (const upgrade of ExportCustoms[payload.ItemType].randomisedUpgrades!) { - buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + const meta = ExportCustoms[payload.ItemType]; + let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] }; + if (meta.subroutines) { + upgradeFingerprint = { + SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1), + ...upgradeFingerprint + }; + } + for (const upgrade of meta.randomisedUpgrades!) { + upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); } const inventoryChanges: IInventoryChanges = addCrewShipSalvagedWeaponSkin( inventory, payload.ItemType, - JSON.stringify({ compat: payload.ItemType, buffs } satisfies IInnateDamageFingerprint) + JSON.stringify(upgradeFingerprint) ); inventoryChanges.CrewShipRawSalvage = [ diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 73156437..d6e2ab34 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -907,6 +907,10 @@ export interface IInnateDamageFingerprint { buffs: IFingerprintStat[]; } +export interface ICrewShipComponentFingerprint extends IInnateDamageFingerprint { + SubroutineIndex?: number; +} + export enum GettingSlotOrderInfo { Empty = "", LotusUpgradesModsRandomizedPlayerMeleeWeaponRandomModRare0 = "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare:0", From ed217bae3358b612ae04fb670393ee59c13bb402 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:38:53 -0700 Subject: [PATCH 510/776] feat: cancel personal tech project (#1679) Closes #1665 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1679 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 257434fb..57370d75 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -28,7 +28,7 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { config } from "@/src/services/configService"; import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; import { GuildMember } from "@/src/models/guildModel"; -import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; @@ -305,6 +305,38 @@ export const guildTechController: RequestHandler = async (req, res) => { guild.ActiveDojoColorResearch = data.RecipeType; await guild.save(); res.end(); + } else if (data.Action == "Cancel" && data.CategoryItemId) { + const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => + x.CategoryItemId?.equals(data.CategoryItemId) + ); + const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex]; + inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); + + const meta = ExportDojoRecipes.research[personalTechProject.ItemType]; + const contributedCredits = meta.price - personalTechProject.ReqCredits; + const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false); + inventoryChanges.MiscItems = []; + for (const ingredient of meta.ingredients) { + const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType); + if (reqItem) { + const contributedItems = ingredient.ItemCount - reqItem.ItemCount; + inventoryChanges.MiscItems.push({ + ItemType: ingredient.ItemType, + ItemCount: contributedItems + }); + } + } + addMiscItems(inventory, inventoryChanges.MiscItems); + + await inventory.save(); + res.json({ + action: "Cancel", + isPersonal: true, + inventoryChanges: inventoryChanges, + personalTech: { + ItemId: toOid(personalTechProject._id) + } + }); } else if (data.Action == "Rush" && data.CategoryItemId) { const inventoryChanges: IInventoryChanges = { ...updateCurrency(inventory, 20, true), From 9940024a0130f6eebe24d76f03c066a8eac74d8d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:39:01 -0700 Subject: [PATCH 511/776] fix: put acquired house version railjack armaments into raw salvage (#1685) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1685 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/inventoryService.ts | 25 +++++++++++++++++++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index e84d1c57..081c89a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.55", + "warframe-public-export-plus": "^0.5.56", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.55", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.55.tgz", - "integrity": "sha512-Gnd4FCBVuxm2xWGfu8xxxqPIPSnnTqiEWlpP3rsFpVlQs09RNFnW2PEX9rCZt0f3SvHBv5ssDDrFlzgBHS1yrA==" + "version": "0.5.56", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.56.tgz", + "integrity": "sha512-px+J7tUm6fkSzwKkvL73ySQReDq9oM1UrHSLM3vbYGBvELM892iBgPYG45okIhScCSdwmmXTiWZTf4x/I4qiNQ==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 58b231d3..afcbe580 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.55", + "warframe-public-export-plus": "^0.5.56", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 391f40ae..71dfa9d1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -539,10 +539,27 @@ export const addItem = async ( } } if (typeName in ExportRailjackWeapons) { - return { - ...addEquipment(inventory, ExportRailjackWeapons[typeName].productCategory, typeName), - ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) - }; + const meta = ExportRailjackWeapons[typeName]; + if (meta.defaultUpgrades?.length) { + // House versions need to be identified to get stats so put them into raw salvage first. + const rawSalvageChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addCrewShipRawSalvage(inventory, rawSalvageChanges); + return { CrewShipRawSalvage: rawSalvageChanges }; + } else { + // Sigma versions can be added directly. + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of CrewShipWeapon: got ${quantity}, expected 1`); + } + return { + ...addEquipment(inventory, meta.productCategory, typeName), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) + }; + } } if (typeName in ExportMisc.creditBundles) { const creditsTotal = ExportMisc.creditBundles[typeName] * quantity; From 66e34b7be9160dcb017f1aa590a6fdf1d60025a7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 07:59:42 -0700 Subject: [PATCH 512/776] feat: identify & repair railjack armaments (#1686) Closes #1676 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1686 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 69 ++++++++++++++----- src/controllers/api/guildTechController.ts | 28 ++++++-- src/services/inventoryService.ts | 2 +- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts index d9d09b75..6cde1cda 100644 --- a/src/controllers/api/crewShipIdentifySalvageController.ts +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -1,33 +1,64 @@ -import { addCrewShipSalvagedWeaponSkin, addCrewShipRawSalvage, getInventory } from "@/src/services/inventoryService"; +import { + addCrewShipSalvagedWeaponSkin, + addCrewShipRawSalvage, + getInventory, + addEquipment +} from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; -import { ICrewShipComponentFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; -import { ExportCustoms } from "warframe-public-export-plus"; +import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getRandomInt } from "@/src/services/rngService"; +import { IFingerprintStat } from "@/src/helpers/rivenHelper"; export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId, "CrewShipSalvagedWeaponSkins CrewShipRawSalvage"); + const inventory = await getInventory( + accountId, + "CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage" + ); const payload = getJSONfromString(String(req.body)); - const meta = ExportCustoms[payload.ItemType]; - let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] }; - if (meta.subroutines) { - upgradeFingerprint = { - SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1), - ...upgradeFingerprint - }; + const inventoryChanges: IInventoryChanges = {}; + if (payload.ItemType in ExportCustoms) { + const meta = ExportCustoms[payload.ItemType]; + let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] }; + if (meta.subroutines) { + upgradeFingerprint = { + SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1), + ...upgradeFingerprint + }; + } + for (const upgrade of meta.randomisedUpgrades!) { + upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } + addCrewShipSalvagedWeaponSkin( + inventory, + payload.ItemType, + JSON.stringify(upgradeFingerprint), + inventoryChanges + ); + } else { + const meta = ExportRailjackWeapons[payload.ItemType]; + const upgradeType = meta.defaultUpgrades![0].ItemType; + const upgradeMeta = ExportUpgrades[upgradeType]; + const buffs: IFingerprintStat[] = []; + for (const buff of upgradeMeta.upgradeEntries!) { + buffs.push({ + Tag: buff.tag, + Value: Math.trunc(Math.random() * 0x40000000) + }); + } + addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, undefined, inventoryChanges, { + UpgradeType: upgradeType, + UpgradeFingerprint: JSON.stringify({ + compat: payload.ItemType, + buffs + } satisfies IInnateDamageFingerprint) + }); } - for (const upgrade of meta.randomisedUpgrades!) { - upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - } - const inventoryChanges: IInventoryChanges = addCrewShipSalvagedWeaponSkin( - inventory, - payload.ItemType, - JSON.stringify(upgradeFingerprint) - ); inventoryChanges.CrewShipRawSalvage = [ { diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 57370d75..ebfd40ea 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -15,6 +15,7 @@ import { ExportDojoRecipes } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addCrewShipWeaponSkin, + addEquipment, addItem, addMiscItems, addRecipes, @@ -382,18 +383,31 @@ interface IGuildTechContributeRequest { const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => { // delete personal tech project const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId)); - if (personalTechProjectIndex != -1) { - inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); - } + const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex]; + inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); + + const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins"; + const salvageCategory = category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; // find salved part & delete it - const crewShipSalvagedWeaponSkinsIndex = inventory.CrewShipSalvagedWeaponSkins.findIndex(x => x._id.equals(itemId)); - const crewShipWeaponSkin = inventory.CrewShipSalvagedWeaponSkins[crewShipSalvagedWeaponSkinsIndex]; - inventory.CrewShipSalvagedWeaponSkins.splice(crewShipSalvagedWeaponSkinsIndex, 1); + const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId)); + const salvageItem = inventory[category][salvageIndex]; + inventory[salvageCategory].splice(salvageIndex, 1); // add final item const inventoryChanges = { - ...addCrewShipWeaponSkin(inventory, crewShipWeaponSkin.ItemType, crewShipWeaponSkin.UpgradeFingerprint), + ...(category == "CrewShipWeaponSkins" + ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) + : addEquipment( + inventory, + category, + salvageItem.ItemType, + undefined, + {}, + { + UpgradeFingerprint: salvageItem.UpgradeFingerprint + } + )), ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 71dfa9d1..2dd4c0d3 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1083,7 +1083,7 @@ export const addEquipment = ( Configs: [], XP: 0, ModularParts: modularParts, - IsNew: category != "CrewShipWeapons" ? true : undefined + IsNew: category != "CrewShipWeapons" && category != "CrewShipSalvagedWeapons" ? true : undefined }, defaultOverwrites ); From 8a1603a661becd0fbea3d5b13fa5351a027280b5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 07:59:57 -0700 Subject: [PATCH 513/776] feat: more comprehensive handling of railjack items in sellController (#1687) Closes #1675 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1687 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/sellController.ts | 108 +++++++++++++++++++++----- 1 file changed, 90 insertions(+), 18 deletions(-) diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index f6840589..138cd3b4 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -7,16 +7,18 @@ import { addMiscItems, addConsumables, freeUpSlot, - combineInventoryChanges + combineInventoryChanges, + addCrewShipRawSalvage } from "@/src/services/inventoryService"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { ExportDojoRecipes } from "warframe-public-export-plus"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; export const sellController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as ISellRequest; const accountId = await getAccountIdForRequest(req); - const requiredFields = new Set(); + const requiredFields = new Set(); if (payload.SellCurrency == "SC_RegularCredits") { requiredFields.add("RegularCredits"); } else if (payload.SellCurrency == "SC_FusionPoints") { @@ -25,7 +27,7 @@ export const sellController: RequestHandler = async (req, res) => { requiredFields.add("MiscItems"); } for (const key of Object.keys(payload.Items)) { - requiredFields.add(key); + requiredFields.add(key as keyof TInventoryDatabaseDocument); } if (requiredFields.has("Upgrades")) { requiredFields.add("RawUpgrades"); @@ -51,8 +53,15 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.Hoverboards) { requiredFields.add(InventorySlot.SPACESUITS); } - if (payload.Items.CrewShipWeapons) { + if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) { requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + requiredFields.add("CrewShipRawSalvage"); + if (payload.Items.CrewShipWeapons) { + requiredFields.add("CrewShipSalvagedWeapons"); + } + if (payload.Items.CrewShipWeaponSkins) { + requiredFields.add("CrewShipSalvagedWeaponSkins"); + } } const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); @@ -76,7 +85,7 @@ export const sellController: RequestHandler = async (req, res) => { } ]); } else if (payload.SellCurrency == "SC_Resources") { - // Will add appropriate MiscItems from CrewShipWeapons + // Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins } else { throw new Error("Unknown SellCurrency: " + payload.SellCurrency); } @@ -157,19 +166,51 @@ export const sellController: RequestHandler = async (req, res) => { } if (payload.Items.CrewShipWeapons) { payload.Items.CrewShipWeapons.forEach(sellItem => { - const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String)); - if (index != -1) { - const itemType = inventory.CrewShipWeapons[index].ItemType; - const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType)!; - const miscItemChanges = recipe.ingredients.map(x => ({ - ItemType: x.ItemType, - ItemCount: Math.trunc(x.ItemCount * 0.8) - })); - addMiscItems(inventory, miscItemChanges); - combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); - - inventory.CrewShipWeapons.splice(index, 1); - freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + if (sellItem.String[0] == "/") { + addCrewShipRawSalvage(inventory, [ + { + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + } + ]); + } else { + const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String)); + if (index != -1) { + if (payload.SellCurrency == "SC_Resources") { + refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges); + } + inventory.CrewShipWeapons.splice(index, 1); + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } else { + inventory.CrewShipSalvagedWeapons.pull({ _id: sellItem.String }); + } + } + }); + } + if (payload.Items.CrewShipWeaponSkins) { + payload.Items.CrewShipWeaponSkins.forEach(sellItem => { + if (sellItem.String[0] == "/") { + addCrewShipRawSalvage(inventory, [ + { + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + } + ]); + } else { + const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String)); + if (index != -1) { + if (payload.SellCurrency == "SC_Resources") { + refundPartialBuildCosts( + inventory, + inventory.CrewShipWeaponSkins[index].ItemType, + inventoryChanges + ); + } + inventory.CrewShipWeaponSkins.splice(index, 1); + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } else { + inventory.CrewShipSalvagedWeaponSkins.pull({ _id: sellItem.String }); + } } }); } @@ -243,6 +284,7 @@ interface ISellRequest { Hoverboards?: ISellItem[]; Drones?: ISellItem[]; CrewShipWeapons?: ISellItem[]; + CrewShipWeaponSkins?: ISellItem[]; }; SellPrice: number; SellCurrency: @@ -259,3 +301,33 @@ interface ISellItem { String: string; // oid or uniqueName Count: number; } + +const refundPartialBuildCosts = ( + inventory: TInventoryDatabaseDocument, + itemType: string, + inventoryChanges: IInventoryChanges +): void => { + // House versions + const research = Object.values(ExportDojoRecipes.research).find(x => x.resultType == itemType); + if (research) { + const miscItemChanges = research.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: Math.trunc(x.ItemCount * 0.8) + })); + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + return; + } + + // Sigma versions + const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType); + if (recipe) { + const miscItemChanges = recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: Math.trunc(x.ItemCount * 0.8) + })); + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + return; + } +}; From 435aafeaaeb9ceaecc7336d74b796b52ac2e62ff Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:00:19 -0700 Subject: [PATCH 514/776] feat: randomly generate 1999 calendar seasons (#1689) also handling week rollover now Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1689 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 225 ++++++++++++++++-- src/types/worldStateTypes.ts | 20 +- .../worldState/1999_fall_days.json | 77 ------ .../worldState/1999_spring_days.json | 75 ------ .../worldState/1999_summer_days.json | 75 ------ .../worldState/1999_winter_days.json | 75 ------ .../worldState/worldState.json | 87 +------ 7 files changed, 227 insertions(+), 407 deletions(-) delete mode 100644 static/fixed_responses/worldState/1999_fall_days.json delete mode 100644 static/fixed_responses/worldState/1999_spring_days.json delete mode 100644 static/fixed_responses/worldState/1999_summer_days.json delete mode 100644 static/fixed_responses/worldState/1999_winter_days.json diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 55f6afd9..ed65a8fc 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1,14 +1,10 @@ import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; -import static1999FallDays from "@/static/fixed_responses/worldState/1999_fall_days.json"; -import static1999SpringDays from "@/static/fixed_responses/worldState/1999_spring_days.json"; -import static1999SummerDays from "@/static/fixed_responses/worldState/1999_summer_days.json"; -import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winter_days.json"; import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes"; +import { ICalendarDay, ICalendarSeason, ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -352,6 +348,209 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; +const birthdays: number[] = [ + 1, // Kaya + 45, // Lettie + 74, // Minerva (MinervaVelemirDialogue_rom.dialogue) + 143, // Amir + 166, // Flare + 191, // Aoi + 306, // Eleanor + 307, // Arthur + 338, // Quincy + 355 // Velimir (MinervaVelemirDialogue_rom.dialogue) +]; + +const getCalendarSeason = (week: number): ICalendarSeason => { + const seasonIndex = week % 4; + const seasonDay1 = seasonIndex * 90 + 1; + const seasonDay91 = seasonIndex * 90 + 91; + const eventDays: ICalendarDay[] = []; + for (const day of birthdays) { + if (day < seasonDay1) { + continue; + } + if (day >= seasonDay91) { + break; + } + //logger.debug(`birthday on day ${day}`); + eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0 + } + const rng = new CRng(week); + const challenges = [ + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeHard" + ]; + const rewardRanges: number[] = []; + const upgradeRanges: number[] = []; + for (let i = 0; i != 6; ++i) { + const chunkDay1 = seasonDay1 + i * 15; + const chunkDay13 = chunkDay1 - 1 + 13; + let challengeDay: number; + do { + challengeDay = rng.randomInt(chunkDay1, chunkDay13); + } while (birthdays.indexOf(challengeDay) != -1); + + const challengeIndex = rng.randomInt(0, challenges.length - 1); + const challenge = challenges[challengeIndex]; + challenges.splice(challengeIndex, 1); + + //logger.debug(`challenge on day ${challengeDay}`); + eventDays.push({ + day: challengeDay, + events: [{ type: "CET_CHALLENGE", challenge }] + }); + + rewardRanges.push(challengeDay); + if (i == 0 || i == 3 || i == 5) { + upgradeRanges.push(challengeDay); + } + } + rewardRanges.push(seasonDay91); + upgradeRanges.push(seasonDay91); + + const rewards = [ + "/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker", + "/Lotus/StoreItems/Types/Recipes/Components/FormaAuraBlueprint", + "/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint", + "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker", + "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle", + "/Lotus/StoreItems/Types/BoosterPacks/CalendarRivenPack", + "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall", + "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleLarge", + "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack", + "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack", + "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem", + "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem", + "/Lotus/Types/StoreItems/Boosters/ResourceDropChance3DayStoreItem", + "/Lotus/StoreItems/Types/Items/MiscItems/Forma", + "/Lotus/StoreItems/Types/Recipes/Components/OrokinCatalystBlueprint", + "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker", + "/Lotus/Types/StoreItems/Packages/Calendar/CalendarVosforPack", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalOrange", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalViolet" + ]; + for (let i = 0; i != rewardRanges.length - 1; ++i) { + const rewardIndex = rng.randomInt(0, rewards.length - 1); + const reward = rewards[rewardIndex]; + rewards.splice(rewardIndex, 1); + + //logger.debug(`trying to fit a reward between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`); + let day: number; + do { + day = rng.randomInt(rewardRanges[i] + 1, rewardRanges[i + 1] - 1); + } while (eventDays.find(x => x.day == day)); + eventDays.push({ day, events: [{ type: "CET_REWARD", reward }] }); + } + + const upgrades = [ + "/Lotus/Upgrades/Calendar/MeleeCritChance", + "/Lotus/Upgrades/Calendar/MeleeAttackSpeed", + "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange", + "/Lotus/Upgrades/Calendar/AbilityStrength", + "/Lotus/Upgrades/Calendar/Armor", + "/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage", + "/Lotus/Upgrades/Calendar/CompanionDamage", + "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary", + "/Lotus/Upgrades/Calendar/MagazineCapacity", + "/Lotus/Upgrades/Calendar/PunchToPrimary", + "/Lotus/Upgrades/Calendar/HealingEffects", + "/Lotus/Upgrades/Calendar/EnergyRestoration", + "/Lotus/Upgrades/Calendar/OvershieldCap", + "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance", + "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier", + "/Lotus/Upgrades/Calendar/MagnetStatusPull", + "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer", + "/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent", + "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup", + "/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee", + "/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy", + "/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts", + "/Lotus/Upgrades/Calendar/CompanionsRadiationChance", + "/Lotus/Upgrades/Calendar/BlastEveryXShots", + "/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill", + "/Lotus/Upgrades/Calendar/ElectricDamagePerDistance", + "/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit", + "/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts", + "/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill", + "/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc", + "/Lotus/Upgrades/Calendar/ExplodingHealthOrbs", + "/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne", + "/Lotus/Upgrades/Calendar/EnergyWavesOnCombo", + "/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent", + "/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent", + "/Lotus/Upgrades/Calendar/GuidingMissilesChance", + "/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield", + "/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump" + ]; + for (let i = 0; i != upgradeRanges.length - 1; ++i) { + const upgradeIndex = rng.randomInt(0, upgrades.length - 1); + const upgrade = upgrades[upgradeIndex]; + upgrades.splice(upgradeIndex, 1); + + //logger.debug(`trying to fit an upgrade between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`); + let day: number; + do { + day = rng.randomInt(upgradeRanges[i] + 1, upgradeRanges[i + 1] - 1); + } while (eventDays.find(x => x.day == day)); + eventDays.push({ day, events: [{ type: "CET_UPGRADE", upgrade }] }); + } + + eventDays.sort((a, b) => a.day - b.day); + + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + return { + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Days: eventDays, + Season: ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][seasonIndex], + YearIteration: Math.trunc(week / 4), + Version: 19, + UpgradeAvaliabilityRequirements: ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"] + }; +}; + export const getWorldState = (buildLabel?: string): IWorldState => { const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc(day / 7); @@ -376,6 +575,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Params: "", ActiveChallenges: [] }, + KnownCalendarSeasons: [], ...staticWorldState, SyndicateMissions: [...staticWorldState.SyndicateMissions] }; @@ -834,17 +1034,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); // 1999 Calendar Season cycling every week + YearIteration every 4 weeks - // TODO: Handle imminent rollover - worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } }; - worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } }; - worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4]; - worldState.KnownCalendarSeasons[0].Days = [ - static1999WinterDays, - static1999SpringDays, - static1999SummerDays, - static1999FallDays - ][week % 4]; - worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4); + worldState.KnownCalendarSeasons.push(getCalendarSeason(week)); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); + } // Sentient Anomaly cycling every 30 minutes const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index aaa292e4..278f6e95 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -127,8 +127,22 @@ export interface ICalendarSeason { Activation: IMongoDate; Expiry: IMongoDate; Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL" - Days: { - day: number; - }[]; + Days: ICalendarDay[]; YearIteration: number; + Version: number; + UpgradeAvaliabilityRequirements: string[]; +} + +export interface ICalendarDay { + day: number; + events: ICalendarEvent[]; +} + +export interface ICalendarEvent { + type: string; + challenge?: string; + reward?: string; + upgrade?: string; + dialogueName?: string; + dialogueConvo?: string; } diff --git a/static/fixed_responses/worldState/1999_fall_days.json b/static/fixed_responses/worldState/1999_fall_days.json deleted file mode 100644 index 3bcd30eb..00000000 --- a/static/fixed_responses/worldState/1999_fall_days.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - { "day": 276, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 283, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance" } - ] - }, - { - "day": 289, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 295, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeEasy" }] }, - { - "day": 302, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" } - ] - }, - { "day": 305, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusMedium" }] }, - { "day": 306, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", "dialogueConvo": "EleanorBirthdayConvo" }] }, - { "day": 307, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", "dialogueConvo": "ArthurBirthdayConvo" }] }, - { - "day": 309, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/Forma" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" } - ] - }, - { - "day": 314, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit" } - ] - }, - { "day": 322, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium" }] }, - { - "day": 328, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 337, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard" }] }, - { "day": 338, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue", "dialogueConvo": "QuincyBirthdayConvo" }] }, - { - "day": 340, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeCritChance" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/AbilityStrength" } - ] - }, - { - "day": 343, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/FormaAura" } - ] - }, - { "day": 352, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard" }] }, - { - "day": 364, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem" } - ] - } -] diff --git a/static/fixed_responses/worldState/1999_spring_days.json b/static/fixed_responses/worldState/1999_spring_days.json deleted file mode 100644 index 4386f2a4..00000000 --- a/static/fixed_responses/worldState/1999_spring_days.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { "day": 100, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 101, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyRestoration" } - ] - }, - { - "day": 102, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 106, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesEasy" }] }, - { - "day": 107, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 122, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeMedium" }] }, - { - "day": 127, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/Forma" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarVosforPack" } - ] - }, - { "day": 129, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium" }] }, - { - "day": 135, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker" } - ] - }, - { - "day": 142, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/BlastEveryXShots" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill" } - ] - }, - { "day": 143, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", "dialogueConvo": "AmirBirthdayConvo" }] }, - { "day": 161, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard" }] }, - { - "day": 165, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/Forma" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem" } - ] - }, - { "day": 169, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsHard" }] }, - { - "day": 171, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/AbilityStrength" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeCritChance" } - ] - }, - { - "day": 176, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" } - ] - } -] diff --git a/static/fixed_responses/worldState/1999_summer_days.json b/static/fixed_responses/worldState/1999_summer_days.json deleted file mode 100644 index 99beee4a..00000000 --- a/static/fixed_responses/worldState/1999_summer_days.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { "day": 186, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { "day": 191, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", "dialogueConvo": "AoiBirthdayConvo" }] }, - { - "day": 193, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { - "day": 197, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeAttackSpeed" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/AbilityStrength" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" } - ] - }, - { "day": 199, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium" }] }, - { - "day": 210, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle" } - ] - }, - { "day": 215, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeEasy" }] }, - { - "day": 228, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarRivenPack" } - ] - }, - { "day": 236, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium" }] }, - { - "day": 237, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleLarge" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { - "day": 240, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionsRadiationChance" } - ] - }, - { "day": 245, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesHard" }] }, - { - "day": 250, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint" } - ] - }, - { "day": 254, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard" }] }, - { - "day": 267, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" } - ] - }, - { - "day": 270, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/PunchToPrimary" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/OvershieldCap" } - ] - } -] diff --git a/static/fixed_responses/worldState/1999_winter_days.json b/static/fixed_responses/worldState/1999_winter_days.json deleted file mode 100644 index 700866d3..00000000 --- a/static/fixed_responses/worldState/1999_winter_days.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { "day": 6, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy" }] }, - { - "day": 15, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MagazineCapacity" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/Armor" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyRestoration" } - ] - }, - { "day": 21, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 25, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen" } - ] - }, - { - "day": 31, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 43, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium" }] }, - { "day": 45, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", "dialogueConvo": "LettieBirthdayConvo" }] }, - { - "day": 47, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 48, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium" }] }, - { - "day": 54, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier" } - ] - }, - { - "day": 56, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 71, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard" }] }, - { - "day": 77, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle" } - ] - }, - { "day": 80, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium" }] }, - { - "day": 83, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker" } - ] - }, - { - "day": 87, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeAttackSpeed" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" } - ] - } -] diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 3b4143e5..dd76dd56 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -2716,90 +2716,5 @@ "ConstructionProjects": [], "TwitchPromos": [], "ExperimentRecommended": [], - "ForceLogoutVersion": 0, - "KnownCalendarSeasons": [ - { - "Activation": { "$date": { "$numberLong": "1733961600000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Days": [ - { "day": 6, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy" }] }, - { - "day": 15, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MagazineCapacity" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/Armor" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyRestoration" } - ] - }, - { "day": 21, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 25, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen" } - ] - }, - { - "day": 31, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 43, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium" }] }, - { "day": 45, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", "dialogueConvo": "LettieBirthdayConvo" }] }, - { - "day": 47, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 48, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium" }] }, - { - "day": 54, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier" } - ] - }, - { - "day": 56, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 71, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard" }] }, - { - "day": 77, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle" } - ] - }, - { "day": 80, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium" }] }, - { - "day": 83, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker" } - ] - }, - { - "day": 87, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeAttackSpeed" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" } - ] - } - ], - "Season": "CST_WINTER", - "YearIteration": 0, - "Version": 17, - "UpgradeAvaliabilityRequirements": ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"] - } - ] + "ForceLogoutVersion": 0 } From 76a53bb1f61fb176594502a506f2efdab23434c1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:00:31 -0700 Subject: [PATCH 515/776] fix: don't consider simaris title 1 to earn a free favour (#1690) Fixes #1688 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1690 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/syndicateSacrificeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index 36ad07cb..fd385999 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -51,7 +51,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp syndicate.Title ??= 0; syndicate.Title += 1; - if (syndicate.Title > 0 && manifest.favours.length != 0) { + if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) { syndicate.FreeFavorsEarned ??= []; if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { syndicate.FreeFavorsEarned.push(syndicate.Title); From 419096f603ee2e97c5aa0afe203cfb64088efba2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:01:59 -0700 Subject: [PATCH 516/776] feat: noDeathMarks cheat (#1691) Closes #1583 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1691 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + src/services/configService.ts | 1 + src/services/missionInventoryUpdateService.ts | 31 ++++++++++--------- static/webui/index.html | 4 +++ 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/zh.js | 1 + 10 files changed, 29 insertions(+), 14 deletions(-) diff --git a/config.json.example b/config.json.example index 9d072329..32562400 100644 --- a/config.json.example +++ b/config.json.example @@ -33,6 +33,7 @@ "noArgonCrystalDecay": false, "noMasteryRankUpCooldown": false, "noVendorPurchaseLimits": true, + "noDeathMarks": false, "noKimCooldowns": false, "instantResourceExtractorDrones": false, "noResourceExtractorDronesDamage": false, diff --git a/src/services/configService.ts b/src/services/configService.ts index cb1a2c38..24707bd5 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -39,6 +39,7 @@ interface IConfig { noArgonCrystalDecay?: boolean; noMasteryRankUpCooldown?: boolean; noVendorPurchaseLimits?: boolean; + noDeathMarks?: boolean; noKimCooldowns?: boolean; instantResourceExtractorDrones?: boolean; noResourceExtractorDronesDamage?: boolean; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 0b452ccc..c30af726 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -54,6 +54,7 @@ import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getWorldState } from "./worldStateService"; +import { config } from "./configService"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C @@ -418,22 +419,24 @@ export const addMissionInventoryUpdates = async ( break; } case "DeathMarks": { - for (const bossName of value) { - if (inventory.DeathMarks.indexOf(bossName) == -1) { - // It's a new death mark; we have to say the line. - await createMessage(inventory.accountOwnerId, [ - { - sub: bossName, - sndr: "/Lotus/Language/G1Quests/DeathMarkSender", - msg: "/Lotus/Language/G1Quests/DeathMarkMessage", - icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", - highPriority: true, - expiry: 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 clear if this is correct. - } - ]); + if (!config.noDeathMarks) { + for (const bossName of value) { + if (inventory.DeathMarks.indexOf(bossName) == -1) { + // It's a new death mark; we have to say the line. + await createMessage(inventory.accountOwnerId, [ + { + sub: bossName, + sndr: "/Lotus/Language/G1Quests/DeathMarkSender", + msg: "/Lotus/Language/G1Quests/DeathMarkMessage", + icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", + highPriority: true, + expiry: 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 clear if this is correct. + } + ]); + } } + inventory.DeathMarks = value; } - inventory.DeathMarks = value; break; } case "CapturedAnimals": { diff --git a/static/webui/index.html b/static/webui/index.html index ec786b4e..3830e378 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -611,6 +611,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 20be9fbd..2357d455 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -138,6 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, + cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, 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 bbf2561a..3b6fb449 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -137,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, + cheats_noDeathMarks: `No Death Marks`, cheats_noKimCooldowns: `No KIM Cooldowns`, 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 c979bddc..8b751375 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -138,6 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, + cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 54f041c6..b1938b53 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -138,6 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 62c82f70..4a5dceeb 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -138,6 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, + cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 3d7af8ec..5af41bea 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -138,6 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, From e38d52fb1bfa5a760ae145309a894dce75181691 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:02:13 -0700 Subject: [PATCH 517/776] feat: sortie reward (#1692) May work somewhat for lite sorties, didn't test that. They'd also need some extra handling with regards to the archon shards with their dynamic probabilities. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1692 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 29 +++++++++++++++++-- src/services/missionInventoryUpdateService.ts | 14 +++++++++ src/types/inventoryTypes/inventoryTypes.ts | 13 +++++++-- src/types/requestTypes.ts | 3 ++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 97a0022e..dcd59ff7 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -86,7 +86,9 @@ import { IWeeklyMission, ILockedWeaponGroupDatabase, IPersonalTechProjectDatabase, - IPersonalTechProjectClient + IPersonalTechProjectClient, + ILastSortieRewardDatabase, + ILastSortieRewardClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1202,6 +1204,28 @@ const alignmentSchema = new Schema( { _id: false } ); +const lastSortieRewardSchema = new Schema( + { + SortieId: Schema.Types.ObjectId, + StoreItem: String, + Manifest: String + }, + { _id: false } +); + +lastSortieRewardSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ILastSortieRewardDatabase; + const client = obj as ILastSortieRewardClient; + + client.SortieId = toOid(db.SortieId); + + delete obj._id; + delete obj.__v; + } +}); + const lockedWeaponGroupSchema = new Schema( { s: Schema.Types.ObjectId, @@ -1419,7 +1443,8 @@ const inventorySchema = new Schema( //https://warframe.fandom.com/wiki/Sortie CompletedSorties: [String], - LastSortieReward: [Schema.Types.Mixed], + LastSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined }, // Resource Extractor Drones Drones: [droneSchema], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c30af726..3ff26b29 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -584,6 +584,16 @@ export const addMissionRewards = async ( const AffiliationMods: IAffiliationMods[] = []; let SyndicateXPItemReward; + if (rewardInfo.sortieTag == "Final") { + inventory.LastSortieReward = [ + { + SortieId: new Types.ObjectId(rewardInfo.sortieId!.split("_")[1]), + StoreItem: MissionRewards[0].StoreItem, + Manifest: "/Lotus/Types/Game/MissionDecks/SortieRewards" + } + ]; + } + let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display if (levelKeyName) { @@ -943,6 +953,10 @@ function getLevelCreditRewards(node: IRegion): number { function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] { const drops: IMissionReward[] = []; + if (RewardInfo.sortieTag == "Final") { + const drop = getRandomRewardByChance(ExportRewards["/Lotus/Types/Game/MissionDecks/SortieRewards"][0])!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + } if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { drops.push({ StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d6e2ab34..ef90dbf5 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -47,6 +47,8 @@ export interface IInventoryDatabase | "BrandedSuits" | "LockedWeaponGroup" | "PersonalTechProjects" + | "LastSortieReward" + | "LastLiteSortieReward" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -79,6 +81,8 @@ export interface IInventoryDatabase BrandedSuits?: Types.ObjectId[]; LockedWeaponGroup?: ILockedWeaponGroupDatabase; PersonalTechProjects: IPersonalTechProjectDatabase[]; + LastSortieReward?: ILastSortieRewardDatabase[]; + LastLiteSortieReward?: ILastSortieRewardDatabase[]; } export interface IQuestKeyDatabase { @@ -277,7 +281,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Wishlist: string[]; Alignment?: IAlignment; CompletedSorties: string[]; - LastSortieReward: ILastSortieReward[]; + LastSortieReward?: ILastSortieRewardClient[]; + LastLiteSortieReward?: ILastSortieRewardClient[]; Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType: string; @@ -724,12 +729,16 @@ export enum Status { StatusStasis = "STATUS_STASIS" } -export interface ILastSortieReward { +export interface ILastSortieRewardClient { SortieId: IOid; StoreItem: string; Manifest: string; } +export interface ILastSortieRewardDatabase extends Omit { + SortieId: Types.ObjectId; +} + export interface ILibraryDailyTaskInfo { EnemyTypes: string[]; EnemyLocTag: string; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index fff164a8..9441f632 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -129,6 +129,9 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; + sortieId?: string; + sortieTag?: string; + sortiePrereqs?: string[]; VaultsCracked?: number; // for Spy missions rewardTier?: number; nightmareMode?: boolean; From f4f1e11b3138adad7bf0f1800659f31c0f227c65 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Thu, 17 Apr 2025 10:46:22 -0700 Subject: [PATCH 518/776] chore(webui): update Spanish translation (#1702) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1702 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 8b751375..f51f506a 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -138,7 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, + cheats_noDeathMarks: `Sin marcas de muerte`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, From f94ecbfbfc86469b687e050c7ffa1edae1b1d45a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:50:24 -0700 Subject: [PATCH 519/776] chore: validate railjack repair start (#1698) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1698 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index ebfd40ea..b2490178 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -97,6 +97,19 @@ export const guildTechController: RequestHandler = async (req, res) => { res.end(); } else { const recipe = ExportDojoRecipes.research[data.RecipeType]; + if (data.TechProductCategory) { + if ( + data.TechProductCategory != "CrewShipWeapons" && + data.TechProductCategory != "CrewShipWeaponSkins" + ) { + throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); + } + if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) { + throw new Error( + `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array` + ); + } + } const techProject = inventory.PersonalTechProjects[ inventory.PersonalTechProjects.push({ @@ -380,6 +393,12 @@ interface IGuildTechContributeRequest { VaultMiscItems: IMiscItem[]; } +const getSalvageCategory = ( + category: "CrewShipWeapons" | "CrewShipWeaponSkins" +): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => { + return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; +}; + const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => { // delete personal tech project const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId)); @@ -387,7 +406,7 @@ const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: s inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins"; - const salvageCategory = category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; + const salvageCategory = getSalvageCategory(category); // find salved part & delete it const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId)); From 41d976d3629f7c84bd8560656605c121e2f8bbf2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:58:40 -0700 Subject: [PATCH 520/776] fix: don't trigger G3 capture when LevelKeyName is present (#1699) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1699 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 3ff26b29..079f2886 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -128,11 +128,16 @@ export const addMissionInventoryUpdates = async ( ]); } } + + // Somewhat heuristically detect G3 capture: + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694 if ( inventoryUpdates.MissionFailed && inventoryUpdates.MissionStatus == "GS_FAILURE" && inventoryUpdates.ObjectiveReached && - !inventoryUpdates.LockedWeaponGroup + !inventoryUpdates.LockedWeaponGroup && + !inventoryUpdates.LevelKeyName ) { const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; From decbbdc81b127ac5ed55ad655de73d10f73c1749 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Thu, 17 Apr 2025 12:17:15 -0700 Subject: [PATCH 521/776] chore(webui): update German translation (#1704) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1704 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 2357d455..abcae96c 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -138,7 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, + cheats_noDeathMarks: `Keine Todesmarkierungen`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, From 79492efbb4942ce7dc06a7693419a13afa9f3862 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:15:50 -0700 Subject: [PATCH 522/776] chore: pass --enable-source-maps to node for npm run start (#1701) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1701 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afcbe580..15f4597c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "WF Emulator", "main": "index.ts", "scripts": { - "start": "node --import ./build/src/pathman.js build/src/index.js", + "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "verify": "tsgo --noEmit", From 0d8f5ee66c00990bfbd645d841debe3117faa397 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:40:41 -0700 Subject: [PATCH 523/776] fix: provide proper response when unbranding a suit (#1697) Fixes #1695 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1697 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/claimCompletedRecipeController.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 880b2267..a2c8139f 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -18,6 +18,7 @@ import { import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { toOid } from "@/src/helpers/inventoryHelpers"; interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -80,6 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } else { logger.debug("Claiming Recipe", { recipe, pendingRecipe }); + let BrandedSuits: undefined | IOid[]; if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { inventory.PendingSpectreLoadouts ??= []; inventory.SpectreLoadouts ??= []; @@ -104,9 +106,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), 1 ); + BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)]; } - let InventoryChanges = {}; + let InventoryChanges: IInventoryChanges = {}; if (recipe.consumeOnUse) { addRecipes(inventory, [ { @@ -134,6 +137,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = }; } await inventory.save(); - res.json({ InventoryChanges }); + res.json({ InventoryChanges, BrandedSuits }); } }; From 379f57be2ca563d48838f209a382a03871671e1f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:40:51 -0700 Subject: [PATCH 524/776] chore: add pumpkin containers to allScans (#1703) Closes #1693 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1703 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/fixed_responses/allScans.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 716c1a3b..f4b43062 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -1092,5 +1092,10 @@ "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerDestroyerAvatar", "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateUltraRare", "/Lotus/Objects/DomestikDrone/GrineerOceanDomestikDroneMover", - "/Lotus/Types/Gameplay/1999Wf/Extermination/SupplyCrate" + "/Lotus/Types/Gameplay/1999Wf/Extermination/SupplyCrate", + "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", + "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem" ] From 196182f9a8ecd166dfaf5465b3fc833dce195fe3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:15:27 -0700 Subject: [PATCH 525/776] feat: acquisition of CrewMembers (#1705) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1705 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 58 +++++++++++++- src/services/inventoryService.ts | 84 +++++++++++++++++++- src/services/purchaseService.ts | 23 ++++-- src/types/inventoryTypes/inventoryTypes.ts | 40 ++++++---- src/types/purchaseTypes.ts | 4 +- 5 files changed, 183 insertions(+), 26 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index dcd59ff7..5faee74a 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -88,7 +88,11 @@ import { IPersonalTechProjectDatabase, IPersonalTechProjectClient, ILastSortieRewardDatabase, - ILastSortieRewardClient + ILastSortieRewardClient, + ICrewMemberSkill, + ICrewMemberSkillEfficiency, + ICrewMemberDatabase, + ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -294,6 +298,55 @@ upgradeSchema.set("toJSON", { } }); +const crewMemberSkillSchema = new Schema( + { + Assigned: Number + }, + { _id: false } +); + +const crewMemberSkillEfficiencySchema = new Schema( + { + PILOTING: crewMemberSkillSchema, + GUNNERY: crewMemberSkillSchema, + ENGINEERING: crewMemberSkillSchema, + COMBAT: crewMemberSkillSchema, + SURVIVABILITY: crewMemberSkillSchema + }, + { _id: false } +); + +const crewMemberSchema = new Schema( + { + ItemType: { type: String, required: true }, + NemesisFingerprint: { type: BigInt, default: 0n }, + Seed: { type: BigInt, default: 0n }, + AssignedRole: Number, + SkillEfficiency: crewMemberSkillEfficiencySchema, + WeaponConfigIdx: Number, + WeaponId: { type: Schema.Types.ObjectId, default: "000000000000000000000000" }, + XP: { type: Number, default: 0 }, + PowersuitType: { type: String, required: true }, + Configs: [ItemConfigSchema], + SecondInCommand: { type: Boolean, default: false } + }, + { id: false } +); + +crewMemberSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ICrewMemberDatabase; + const client = obj as ICrewMemberClient; + + client.WeaponId = toOid(db.WeaponId); + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + const slotsBinSchema = new Schema( { Slots: Number, @@ -1363,7 +1416,7 @@ const inventorySchema = new Schema( CrewShipSalvagedWeaponSkins: [upgradeSchema], //RailJack Crew - CrewMembers: [Schema.Types.Mixed], + CrewMembers: [crewMemberSchema], //Complete Mission\Quests Missions: [missionSchema], @@ -1645,6 +1698,7 @@ export type InventoryDocumentProps = { CrewShipWeaponSkins: Types.DocumentArray; CrewShipSalvagedWeaponSkins: Types.DocumentArray; PersonalTechProjects: Types.DocumentArray; + CrewMembers: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2dd4c0d3..7537c8d6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -22,7 +22,8 @@ import { IDroneClient, IUpgradeClient, TPartialStartingGear, - ILoreFragmentScan + ILoreFragmentScan, + ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -713,6 +714,15 @@ export const addItem = async ( return { MiscItems: miscItemChanges }; + } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { + if (!seed) { + throw new Error(`Expected crew member to have a seed`); + } + seed |= 0x33b81en << 32n; + return { + ...addCrewMember(inventory, typeName, seed), + ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase) + }; } else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") { return addCrewShipHarness(inventory, typeName); } @@ -1212,6 +1222,78 @@ const addDrone = ( return inventoryChanges; }; +/*const getCrewMemberSkills = (seed: bigint, skillPointsToAssign: number): Record => { + const rng = new SRng(seed); + + const skills = ["PILOTING", "GUNNERY", "ENGINEERING", "COMBAT", "SURVIVABILITY"]; + for (let i = 1; i != 5; ++i) { + const swapIndex = rng.randomInt(0, i); + if (swapIndex != i) { + const tmp = skills[i]; + skills[i] = skills[swapIndex]; + skills[swapIndex] = tmp; + } + } + + rng.randomFloat(); // unused afaict + + const skillAssignments = [0, 0, 0, 0, 0]; + for (let skill = 0; skillPointsToAssign; skill = (skill + 1) % 5) { + const maxIncrease = Math.min(5 - skillAssignments[skill], skillPointsToAssign); + const increase = rng.randomInt(0, maxIncrease); + skillAssignments[skill] += increase; + skillPointsToAssign -= increase; + } + + skillAssignments.sort((a, b) => b - a); + + const combined: Record = {}; + for (let i = 0; i != 5; ++i) { + combined[skills[i]] = skillAssignments[i]; + } + return combined; +};*/ + +const addCrewMember = ( + inventory: TInventoryDatabaseDocument, + itemType: string, + seed: bigint, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + // SkillEfficiency is additional to the base stats, so we don't need to compute this + //const skillPointsToAssign = itemType.endsWith("Strong") ? 12 : itemType.indexOf("Medium") != -1 ? 10 : 8; + //const skills = getCrewMemberSkills(seed, skillPointsToAssign); + + // Arbiters = male + // CephalonSuda = female + // NewLoka = female + // Perrin = male + // RedVeil = male + // SteelMeridian = female + const powersuitType = + itemType.indexOf("Arbiters") != -1 || itemType.indexOf("Perrin") != -1 || itemType.indexOf("RedVeil") != -1 + ? "/Lotus/Powersuits/NpcPowersuits/CrewMemberMaleSuit" + : "/Lotus/Powersuits/NpcPowersuits/CrewMemberFemaleSuit"; + + const index = + inventory.CrewMembers.push({ + ItemType: itemType, + NemesisFingerprint: 0n, + Seed: seed, + SkillEfficiency: { + PILOTING: { Assigned: 0 }, + GUNNERY: { Assigned: 0 }, + ENGINEERING: { Assigned: 0 }, + COMBAT: { Assigned: 0 }, + SURVIVABILITY: { Assigned: 0 } + }, + PowersuitType: powersuitType + }) - 1; + inventoryChanges.CrewMembers ??= []; + inventoryChanges.CrewMembers.push(inventory.CrewMembers[index].toJSON()); + return inventoryChanges; +}; + export const addEmailItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 32f3af38..6c3b6d8c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -141,7 +141,8 @@ export const handlePurchase = async ( inventory, purchaseRequest.PurchaseParams.Quantity, undefined, - undefined, + false, + purchaseRequest.PurchaseParams.UsePremium, seed ); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); @@ -331,6 +332,7 @@ export const handleStoreItemAcquisition = async ( quantity: number = 1, durability: TRarity = "COMMON", ignorePurchaseQuantity: boolean = false, + premiumPurchase: boolean = true, seed?: bigint ): Promise => { let purchaseResponse = { @@ -352,11 +354,20 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; + purchaseResponse = { + InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed) + }; break; } case "Types": - purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity); + purchaseResponse = await handleTypesPurchase( + internalName, + inventory, + quantity, + ignorePurchaseQuantity, + premiumPurchase, + seed + ); break; case "Boosters": purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); @@ -478,13 +489,15 @@ const handleTypesPurchase = async ( typesName: string, inventory: TInventoryDatabaseDocument, quantity: number, - ignorePurchaseQuantity: boolean + ignorePurchaseQuantity: boolean, + premiumPurchase: boolean = true, + seed?: bigint ): Promise => { const typeCategory = getStoreItemTypesCategory(typesName); logger.debug(`type category ${typeCategory}`); switch (typeCategory) { default: - return { InventoryChanges: await addItem(inventory, typesName, quantity) }; + return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) }; case "BoosterPacks": return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index ef90dbf5..d2201b73 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -49,6 +49,7 @@ export interface IInventoryDatabase | "PersonalTechProjects" | "LastSortieReward" | "LastLiteSortieReward" + | "CrewMembers" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -83,6 +84,7 @@ export interface IInventoryDatabase PersonalTechProjects: IPersonalTechProjectDatabase[]; LastSortieReward?: ILastSortieRewardDatabase[]; LastLiteSortieReward?: ILastSortieRewardDatabase[]; + CrewMembers: ICrewMemberDatabase[]; } export interface IQuestKeyDatabase { @@ -324,7 +326,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu InfestedFoundry?: IInfestedFoundryClient; BlessingCooldown?: IMongoDate; CrewShipRawSalvage: ITypeCount[]; - CrewMembers: ICrewMember[]; + CrewMembers: ICrewMemberClient[]; LotusCustomization: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; @@ -461,13 +463,24 @@ export interface ICompletedJob { StageCompletions: number[]; } -export interface ICrewMember { +export interface ICrewMemberSkill { + Assigned: number; +} + +export interface ICrewMemberSkillEfficiency { + PILOTING: ICrewMemberSkill; + GUNNERY: ICrewMemberSkill; + ENGINEERING: ICrewMemberSkill; + COMBAT: ICrewMemberSkill; + SURVIVABILITY: ICrewMemberSkill; +} + +export interface ICrewMemberClient { ItemType: string; - NemesisFingerprint: number; - Seed: number; - HireDate: IMongoDate; - AssignedRole: number; - SkillEfficiency: ISkillEfficiency; + NemesisFingerprint: bigint; + Seed: bigint; + AssignedRole?: number; + SkillEfficiency: ICrewMemberSkillEfficiency; WeaponConfigIdx: number; WeaponId: IOid; XP: number; @@ -477,16 +490,9 @@ export interface ICrewMember { ItemId: IOid; } -export interface ISkillEfficiency { - PILOTING: ICombat; - GUNNERY: ICombat; - ENGINEERING: ICombat; - COMBAT: ICombat; - SURVIVABILITY: ICombat; -} - -export interface ICombat { - Assigned: number; +export interface ICrewMemberDatabase extends Omit { + WeaponId: Types.ObjectId; + _id: Types.ObjectId; } export enum InventorySlot { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 1b4c9aca..eac812a2 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -6,7 +6,8 @@ import { INemesisClient, ITypeCount, IRecentVendorPurchaseClient, - TEquipmentKey + TEquipmentKey, + ICrewMemberClient } from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { @@ -47,6 +48,7 @@ export type IInventoryChanges = { Nemesis?: Partial; NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 + CrewMembers?: ICrewMemberClient[]; } & Record< Exclude< string, From 3baf6ad0153bb84c5fe6b807aa709d65fbb41548 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:15:50 -0700 Subject: [PATCH 526/776] feat: handle railjack armaments, crew, & customizations in saveLoadout (#1706) Closes #467 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1706 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 25 +++++++++---------- src/services/importService.ts | 16 ++++++------ src/services/saveLoadoutService.ts | 26 ++++++++++++++++++-- src/types/inventoryTypes/inventoryTypes.ts | 23 ++++++++--------- src/types/saveLoadoutTypes.ts | 19 ++++++++++++-- 5 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5faee74a..a2963001 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -39,10 +39,9 @@ import { ILoreFragmentScan, IEvolutionProgress, IEndlessXpProgress, - ICrewShipPortGuns, ICrewShipCustomization, ICrewShipWeapon, - ICrewShipPilotWeapon, + ICrewShipWeaponEmplacements, IShipExterior, IHelminthFoodRecord, ICrewShipMembersDatabase, @@ -772,25 +771,23 @@ const endlessXpProgressSchema = new Schema( { _id: false } ); -const crewShipPilotWeaponSchema = new Schema( +const crewShipWeaponEmplacementsSchema = new Schema( { PRIMARY_A: EquipmentSelectionSchema, - SECONDARY_A: EquipmentSelectionSchema - }, - { _id: false } -); - -const crewShipPortGunsSchema = new Schema( - { - PRIMARY_A: EquipmentSelectionSchema + PRIMARY_B: EquipmentSelectionSchema, + SECONDARY_A: EquipmentSelectionSchema, + SECONDARY_B: EquipmentSelectionSchema }, { _id: false } ); const crewShipWeaponSchema = new Schema( { - PILOT: crewShipPilotWeaponSchema, - PORT_GUNS: crewShipPortGunsSchema + PILOT: crewShipWeaponEmplacementsSchema, + PORT_GUNS: crewShipWeaponEmplacementsSchema, + STARBOARD_GUNS: crewShipWeaponEmplacementsSchema, + ARTILLERY: crewShipWeaponEmplacementsSchema, + SCANNER: crewShipWeaponEmplacementsSchema }, { _id: false } ); @@ -814,7 +811,7 @@ const crewShipCustomizationSchema = new Schema( const crewShipMemberSchema = new Schema( { ItemId: { type: Schema.Types.ObjectId, required: false }, - NemesisFingerprint: { type: Number, required: false } + NemesisFingerprint: { type: BigInt, required: false } }, { _id: false } ); diff --git a/src/services/importService.ts b/src/services/importService.ts index e4f6b97c..0f5dd28d 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -104,18 +104,18 @@ const replaceSlots = (db: ISlots, client: ISlots): void => { db.Slots = client.Slots; }; -const convertCrewShipMember = (client: ICrewShipMemberClient): ICrewShipMemberDatabase => { - return { - ...client, - ItemId: client.ItemId ? new Types.ObjectId(client.ItemId.$oid) : undefined - }; +export const importCrewMemberId = (crewMemberId: ICrewShipMemberClient): ICrewShipMemberDatabase => { + if (crewMemberId.ItemId) { + return { ItemId: new Types.ObjectId(crewMemberId.ItemId.$oid) }; + } + return { NemesisFingerprint: BigInt(crewMemberId.NemesisFingerprint ?? 0) }; }; const convertCrewShipMembers = (client: ICrewShipMembersClient): ICrewShipMembersDatabase => { return { - SLOT_A: client.SLOT_A ? convertCrewShipMember(client.SLOT_A) : undefined, - SLOT_B: client.SLOT_B ? convertCrewShipMember(client.SLOT_B) : undefined, - SLOT_C: client.SLOT_C ? convertCrewShipMember(client.SLOT_C) : undefined + SLOT_A: client.SLOT_A ? importCrewMemberId(client.SLOT_A) : undefined, + SLOT_B: client.SLOT_B ? importCrewMemberId(client.SLOT_B) : undefined, + SLOT_C: client.SLOT_C ? importCrewMemberId(client.SLOT_C) : undefined }; }; diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 472c599d..1f860433 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -13,6 +13,8 @@ import { Types } from "mongoose"; import { isEmptyObject } from "@/src/helpers/general"; import { logger } from "@/src/utils/logger"; import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; +import { importCrewMemberId } from "./importService"; //TODO: setup default items on account creation or like originally in giveStartingItems.php @@ -174,8 +176,8 @@ export const handleInventoryItemConfigChange = async ( } for (const [configId, config] of Object.entries(itemConfigEntries)) { - if (typeof config !== "boolean") { - inventoryItem.Configs[parseInt(configId)] = config; + if (/^[0-9]+$/.test(configId)) { + inventoryItem.Configs[parseInt(configId)] = config as IItemConfig; } } if ("Favorite" in itemConfigEntries) { @@ -184,6 +186,26 @@ export const handleInventoryItemConfigChange = async ( if ("IsNew" in itemConfigEntries) { inventoryItem.IsNew = itemConfigEntries.IsNew; } + + if ("ItemName" in itemConfigEntries) { + inventoryItem.ItemName = itemConfigEntries.ItemName; + } + if ("RailjackImage" in itemConfigEntries) { + inventoryItem.RailjackImage = itemConfigEntries.RailjackImage; + } + if ("Customization" in itemConfigEntries) { + inventoryItem.Customization = itemConfigEntries.Customization; + } + if ("Weapon" in itemConfigEntries) { + inventoryItem.Weapon = itemConfigEntries.Weapon; + } + if (itemConfigEntries.CrewMembers) { + inventoryItem.CrewMembers = { + SLOT_A: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_A ?? {}), + SLOT_B: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_B ?? {}), + SLOT_C: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_C ?? {}) + }; + } } break; } else { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d2201b73..38ee575d 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -538,12 +538,12 @@ export interface ICrewShipMembersDatabase { export interface ICrewShipMemberClient { ItemId?: IOid; - NemesisFingerprint?: number; + NemesisFingerprint?: number | bigint; } export interface ICrewShipMemberDatabase { ItemId?: Types.ObjectId; - NemesisFingerprint?: number; + NemesisFingerprint?: bigint; } export interface ICrewShipCustomization { @@ -568,17 +568,18 @@ export type IMiscItem = ITypeCount; // inventory.CrewShips[0].Weapon export interface ICrewShipWeapon { - PILOT: ICrewShipPilotWeapon; - PORT_GUNS: ICrewShipPortGuns; + PILOT?: ICrewShipWeaponEmplacements; + PORT_GUNS?: ICrewShipWeaponEmplacements; + STARBOARD_GUNS?: ICrewShipWeaponEmplacements; + ARTILLERY?: ICrewShipWeaponEmplacements; + SCANNER?: ICrewShipWeaponEmplacements; } -export interface ICrewShipPilotWeapon { - PRIMARY_A: IEquipmentSelection; - SECONDARY_A: IEquipmentSelection; -} - -export interface ICrewShipPortGuns { - PRIMARY_A: IEquipmentSelection; +export interface ICrewShipWeaponEmplacements { + PRIMARY_A?: IEquipmentSelection; + PRIMARY_B?: IEquipmentSelection; + SECONDARY_A?: IEquipmentSelection; + SECONDARY_B?: IEquipmentSelection; } export interface IDiscoveredMarker { diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index 61f1aba4..dcd1f1b8 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -1,7 +1,13 @@ import { IOid } from "@/src/types/commonTypes"; import { IItemConfig, IOperatorConfigClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { Types } from "mongoose"; -import { ILoadoutConfigClient } from "./inventoryTypes/inventoryTypes"; +import { + ICrewShipCustomization, + ICrewShipMembersClient, + ICrewShipWeapon, + IFlavourItem, + ILoadoutConfigClient +} from "./inventoryTypes/inventoryTypes"; export interface ISaveLoadoutRequest { LoadOuts: ILoadoutClient; @@ -51,7 +57,16 @@ export interface IItemEntry { export type IConfigEntry = { [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; -} & { Favorite?: boolean; IsNew?: boolean }; +} & { + Favorite?: boolean; + IsNew?: boolean; + // Railjack + ItemName?: string; + RailjackImage?: IFlavourItem; + Customization?: ICrewShipCustomization; + Weapon?: ICrewShipWeapon; + CrewMembers?: ICrewShipMembersClient; +}; export type ILoadoutClient = Omit; From 0c34c87d75e644525ba7719fe82e1471d9396f78 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:16:11 -0700 Subject: [PATCH 527/776] fix: give defaultUpgrades for infested pets (#1710) Fixes #1709 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1710 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/modularWeaponCraftingController.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index bab27b23..8819a2a6 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -17,7 +17,7 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { getRandomInt } from "@/src/services/rngService"; -import { ExportSentinels } from "warframe-public-export-plus"; +import { ExportSentinels, IDefaultUpgrade } from "warframe-public-export-plus"; import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { @@ -34,10 +34,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const category = modularWeaponTypes[data.WeaponType]; const inventory = await getInventory(accountId); - const defaultUpgrades = getDefaultUpgrades(data.Parts); - const defaultOverwrites: Partial = { - Configs: applyDefaultUpgrades(inventory, defaultUpgrades) - }; + let defaultUpgrades: IDefaultUpgrade[] | undefined; + const defaultOverwrites: Partial = {}; const inventoryChanges: IInventoryChanges = {}; if (category == "KubrowPets") { const traits = { @@ -129,10 +127,17 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) // Only save mutagen & antigen in the ModularParts. defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]]; - for (const specialItem of ExportSentinels[data.WeaponType].exalted!) { + const meta = ExportSentinels[data.WeaponType]; + + for (const specialItem of meta.exalted!) { addSpecialItem(inventory, specialItem, inventoryChanges); } + + defaultUpgrades = meta.defaultUpgrades; + } else { + defaultUpgrades = getDefaultUpgrades(data.Parts); } + defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false)); if (defaultUpgrades) { From f549b042d6ef891c23ec8bdbfa9fed0315637f3d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:16:43 -0700 Subject: [PATCH 528/776] feat: ignore list (#1711) Closes #1707 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1711 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/addIgnoredUserController.ts | 30 +++++++++++++++++++ .../api/getIgnoredUsersController.ts | 28 +++++++++-------- .../api/removeIgnoredUserController.ts | 21 +++++++++++++ .../custom/deleteAccountController.ts | 4 ++- src/models/loginModel.ts | 12 +++++++- src/routes/api.ts | 4 +++ src/types/guildTypes.ts | 4 +-- src/types/loginTypes.ts | 7 +++++ src/utils/async-utils.ts | 1 + 9 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 src/controllers/api/addIgnoredUserController.ts create mode 100644 src/controllers/api/removeIgnoredUserController.ts diff --git a/src/controllers/api/addIgnoredUserController.ts b/src/controllers/api/addIgnoredUserController.ts new file mode 100644 index 00000000..99b38972 --- /dev/null +++ b/src/controllers/api/addIgnoredUserController.ts @@ -0,0 +1,30 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const addIgnoredUserController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const ignoreeAccount = await Account.findOne( + { DisplayName: data.playerName.substring(0, data.playerName.length - 1) }, + "_id" + ); + if (ignoreeAccount) { + await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id }); + res.json({ + Ignored: { + _id: toOid(ignoreeAccount._id), + DisplayName: data.playerName + } satisfies IFriendInfo + }); + } else { + res.status(400).end(); + } +}; + +interface IAddIgnoredUserRequest { + playerName: string; +} diff --git a/src/controllers/api/getIgnoredUsersController.ts b/src/controllers/api/getIgnoredUsersController.ts index 97127fba..abadb91c 100644 --- a/src/controllers/api/getIgnoredUsersController.ts +++ b/src/controllers/api/getIgnoredUsersController.ts @@ -1,16 +1,20 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/guildTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; import { RequestHandler } from "express"; -const getIgnoredUsersController: RequestHandler = (_req, res) => { - res.writeHead(200, { - "Content-Type": "text/html", - "Content-Length": "3" +export const getIgnoredUsersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const ignores = await Ignore.find({ ignorer: accountId }); + const ignoredUsers: IFriendInfo[] = []; + await parallelForeach(ignores, async ignore => { + const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!; + ignoredUsers.push({ + _id: toOid(ignore.ignoree), + DisplayName: ignoreeAccount.DisplayName + "" + }); }); - res.end( - Buffer.from([ - 0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32, - 0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d - ]) - ); + res.json({ IgnoredUsers: ignoredUsers }); }; - -export { getIgnoredUsersController }; diff --git a/src/controllers/api/removeIgnoredUserController.ts b/src/controllers/api/removeIgnoredUserController.ts new file mode 100644 index 00000000..73613ce6 --- /dev/null +++ b/src/controllers/api/removeIgnoredUserController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const removeIgnoredUserController: RequestHandler = async (req, res) => { + const accountId = await getAccountForRequest(req); + const data = getJSONfromString(String(req.body)); + const ignoreeAccount = await Account.findOne( + { DisplayName: data.playerName.substring(0, data.playerName.length - 1) }, + "_id" + ); + if (ignoreeAccount) { + await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id }); + } + res.end(); +}; + +interface IRemoveIgnoredUserRequest { + playerName: string; +} diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index fad4485b..32fe4f19 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -1,6 +1,6 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { Account } from "@/src/models/loginModel"; +import { Account, Ignore } from "@/src/models/loginModel"; import { Inbox } from "@/src/models/inboxModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; @@ -23,6 +23,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => { await Promise.all([ Account.deleteOne({ _id: accountId }), GuildMember.deleteMany({ accountId: accountId }), + Ignore.deleteMany({ ignorer: accountId }), + Ignore.deleteMany({ ignoree: accountId }), Inbox.deleteMany({ ownerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }), Leaderboard.deleteMany({ ownerId: accountId }), diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 7b12c07a..3e564aa6 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -1,4 +1,4 @@ -import { IDatabaseAccountJson } from "@/src/types/loginTypes"; +import { IDatabaseAccountJson, IIgnore } from "@/src/types/loginTypes"; import { model, Schema, SchemaOptions } from "mongoose"; const opts = { @@ -37,3 +37,13 @@ databaseAccountSchema.set("toJSON", { }); export const Account = model("Account", databaseAccountSchema); + +const ignoreSchema = new Schema({ + ignorer: Schema.Types.ObjectId, + ignoree: Schema.Types.ObjectId +}); + +ignoreSchema.index({ ignorer: 1 }); +ignoreSchema.index({ ignorer: 1, ignoree: 1 }, { unique: true }); + +export const Ignore = model("Ignore", ignoreSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index d16ea321..71b90dc4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController"; import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -97,6 +98,7 @@ import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCode import { releasePetController } from "@/src/controllers/api/releasePetController"; import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; +import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; @@ -202,6 +204,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); @@ -266,6 +269,7 @@ apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); +apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 0d02b0db..59e7a3a0 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -103,12 +103,12 @@ export interface IGuildMemberDatabase { ShipDecorationsContributed?: ITypeCount[]; } -interface IFriendInfo { +export interface IFriendInfo { _id: IOid; DisplayName?: string; PlatformNames?: string[]; PlatformAccountId?: string; - Status: number; + Status?: number; ActiveAvatarImageType?: string; LastLogin?: IMongoDate; PlayerLevel?: number; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 8c443797..17128e2a 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -1,3 +1,5 @@ +import { Types } from "mongoose"; + export interface IAccountAndLoginResponseCommons { DisplayName: string; CountryCode: string; @@ -56,3 +58,8 @@ export interface IGroup { experiment: string; experimentGroup: string; } + +export interface IIgnore { + ignorer: Types.ObjectId; + ignoree: Types.ObjectId; +} diff --git a/src/utils/async-utils.ts b/src/utils/async-utils.ts index b2d40c0d..f8825612 100644 --- a/src/utils/async-utils.ts +++ b/src/utils/async-utils.ts @@ -1,3 +1,4 @@ +// Misnomer: We have concurrency, not parallelism - oh well! export const parallelForeach = async (data: T[], op: (datum: T) => Promise): Promise => { const promises: Promise[] = []; for (const datum of data) { From a6d4fab59551c50fb63fd42ec6018413f4ecb09d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:16:58 -0700 Subject: [PATCH 529/776] chore: rewrite gruzzling droptable to scathing/mocking whispers (#1712) Closes #1708 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1712 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 079f2886..c88e8d11 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -700,6 +700,12 @@ export const addMissionRewards = async ( if (strippedItems) { for (const si of strippedItems) { + if (si.DropTable == "/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable") { + logger.debug( + `rewriting ${si.DropTable} to /Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable` + ); + si.DropTable = "/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable"; + } const droptables = ExportEnemies.droptables[si.DropTable] ?? []; if (si.DROP_MOD) { const modDroptable = droptables.find(x => x.type == "mod"); From de5fd5fce08e8f9847ed7d56a7fc59ab76a9f35c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:17:19 -0700 Subject: [PATCH 530/776] chore: provide a proper schema for CurrentLoadOutIds (#1714) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1714 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 4 ++-- src/models/inventoryModels/loadoutModel.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a2963001..ac9edf1e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -105,7 +105,7 @@ import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; -import { EquipmentSelectionSchema } from "./loadoutModel"; +import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel"; export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); @@ -1588,7 +1588,7 @@ const inventorySchema = new Schema( HasContributedToDojo: Boolean, HWIDProtectEnabled: Boolean, LoadOutPresets: { type: Schema.Types.ObjectId, ref: "Loadout" }, - CurrentLoadOutIds: [Schema.Types.Mixed], + CurrentLoadOutIds: [oidSchema], RandomUpgradesIdentified: Number, BountyScore: Number, ChallengeInstanceStates: [Schema.Types.Mixed], diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts index 73343c8b..208b9e17 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -3,7 +3,7 @@ import { IEquipmentSelection } from "@/src/types/inventoryTypes/commonInventoryT import { ILoadoutConfigDatabase, ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { Document, Model, Schema, Types, model } from "mongoose"; -const oidSchema = new Schema( +export const oidSchema = new Schema( { $oid: String }, From bc5dc02fc9d39dffc0455b39ab4bcc02d627e301 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:17:36 -0700 Subject: [PATCH 531/776] chore: fill in guild member data asynchronously (#1715) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1715 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/guildService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index e490c304..d409654d 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -59,6 +59,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s const members: IGuildMemberClient[] = []; let missingEntry = true; + const dataFillInPromises: Promise[] = []; for (const guildMember of guildMembers) { const member: IGuildMemberClient = { _id: toOid(guildMember.accountId), @@ -70,8 +71,12 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s if (guildMember.accountId.equals(accountId)) { missingEntry = false; } else { - member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; - await fillInInventoryDataForGuildMember(member); + dataFillInPromises.push( + (async (): Promise => { + member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; + })() + ); + dataFillInPromises.push(fillInInventoryDataForGuildMember(member)); } members.push(member); } @@ -90,6 +95,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s }); } + await Promise.all(dataFillInPromises); + return { _id: toOid(guild._id), Name: guild.Name, From 6394adb0f08881eb05ad417c182431bafa5fe8f2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:17:55 -0700 Subject: [PATCH 532/776] fix(webui): handle config get request failing due to expired authz (#1716) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1716 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index b56e2f6e..e8e4821e 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1315,7 +1315,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () { interval = setInterval(() => { if (window.authz) { clearInterval(interval); - fetch("/custom/config?" + window.authz).then(res => { + fetch("/custom/config?" + window.authz).then(async res => { if (res.status == 200) { $("#server-settings-no-perms").addClass("d-none"); $("#server-settings").removeClass("d-none"); @@ -1335,8 +1335,16 @@ single.getRoute("/webui/cheats").on("beforeload", function () { }) ); } else { - $("#server-settings-no-perms").removeClass("d-none"); - $("#server-settings").addClass("d-none"); + if ((await res.text()) == "Log-in expired") { + revalidateAuthz(() => { + if (single.getCurrentPath() == "/webui/cheats") { + single.loadRoute("/webui/cheats"); + } + }); + } else { + $("#server-settings-no-perms").removeClass("d-none"); + $("#server-settings").addClass("d-none"); + } } }); } From a98e18d51156c61c4a42bfaa9e4b6eedd0b96f3c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:18:11 -0700 Subject: [PATCH 533/776] feat: tenet weapon vendor rotation (#1717) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1717 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 27 ++-- ...ubsPerrinSequenceWeaponVendorManifest.json | 133 ------------------ 2 files changed, 19 insertions(+), 141 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index d1dc39e0..f6007642 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -34,7 +34,6 @@ const rawVendorManifests: IRawVendorManifest[] = [ getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing - getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"), getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), getVendorManifestJson("MaskSalesmanManifest"), getVendorManifestJson("Nova1999ConquestShopManifest"), @@ -51,7 +50,8 @@ const rawVendorManifests: IRawVendorManifest[] = [ ]; interface IGeneratableVendorInfo extends Omit { - cycleDuration?: number; + cycleStart: number; + cycleDuration: number; } const generatableVendors: IGeneratableVendorInfo[] = [ @@ -62,6 +62,16 @@ const generatableVendors: IGeneratableVendorInfo[] = [ RandomSeedType: "VRST_WEAPON", RequiredGoalTag: "", WeaponUpgradeValueAttenuationExponent: 2.25, + cycleStart: 1740960000_000, + cycleDuration: 4 * unixTimesInMs.day + }, + { + _id: { $oid: "60ad3b6ec96976e97d227e19" }, + TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", + PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70", + RandomSeedType: "VRST_WEAPON", + WeaponUpgradeValueAttenuationExponent: 2.25, + cycleStart: 1744934400_000, cycleDuration: 4 * unixTimesInMs.day } // { @@ -124,7 +134,7 @@ const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendor const refreshExpiry = (expiry: IMongoDate): number => { const period = parseInt(expiry.$date.$numberLong); if (Date.now() >= period) { - const epoch = 1734307200 * 1000; // Monday (for weekly schedules) + const epoch = 1734307200_000; // Monday (for weekly schedules) const iteration = Math.trunc((Date.now() - epoch) / period); const start = epoch + iteration * period; const end = start + period; @@ -135,11 +145,11 @@ const refreshExpiry = (expiry: IMongoDate): number => { }; const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { - const EPOCH = 1740960000 * 1000; // Monday; aligns with coda weapons 8 day cycle. + const EPOCH = vendorInfo.cycleStart; const manifest = ExportVendors[vendorInfo.TypeName]; let binThisCycle; if (manifest.isOneBinPerCycle) { - const cycleDuration = vendorInfo.cycleDuration!; // manifest.items[0].durationHours! * 3600_000; + const cycleDuration = vendorInfo.cycleDuration; // manifest.items[0].durationHours! * 3600_000; const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. } @@ -150,7 +160,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) { continue; } - const cycleDuration = vendorInfo.cycleDuration!; // rawItem.durationHours! * 3600_000; + const cycleDuration = vendorInfo.cycleDuration; // rawItem.durationHours! * 3600_000; const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); const cycleStart = EPOCH + cycleIndex * cycleDuration; const cycleEnd = cycleStart + cycleDuration; @@ -181,10 +191,11 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani items.push(item); } } - delete vendorInfo.cycleDuration; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { cycleStart, cycleDuration, ...clientVendorInfo } = vendorInfo; return { VendorInfo: { - ...vendorInfo, + ...clientVendorInfo, ItemManifest: items, Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } } } diff --git a/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json deleted file mode 100644 index 1cae38e4..00000000 --- a/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "60ad3b6ec96976e97d227e19" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/BoardExec/Primary/CrpBEFerrox/CrpBEFerrox", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 4383829823946960400, - "Id": { - "$oid": "66fd60b20ba592c4c95e9488" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpBriefcaseScythe/CrpBriefcaseScythe", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 7952272124248276000, - "Id": { - "$oid": "66fd60b20ba592c4c95e9489" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpBriefcase2HKatana/CrpBriefcase2HKatana", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 465952672558014140, - "Id": { - "$oid": "66fd60b20ba592c4c95e948a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/CrpBigSlash/CrpBigSlash", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 8342430883077507000, - "Id": { - "$oid": "66fd60b20ba592c4c95e948b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/ShieldAndSword/CrpHammerShield/CrpHammerShield", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 7441523153174502000, - "Id": { - "$oid": "66fd60b20ba592c4c95e948c" - } - } - ], - "PropertyTextHash": "34F8CF1DFF745F0D67433A5EF0A03E70", - "RandomSeedType": "VRST_WEAPON", - "WeaponUpgradeValueAttenuationExponent": 2.25, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} From da6067ec43befb3788bb91aa7d964af45dcebbd6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:18:26 -0700 Subject: [PATCH 534/776] fix: use correct drop table for phorid assassination (#1718) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1718 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 14 ++++++++++---- src/types/requestTypes.ts | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c88e8d11..b75522c2 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -976,10 +976,16 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u } if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; - let rewardManifests: string[] = - RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB" - ? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"] - : region.rewardManifests; + let rewardManifests: string[]; + if (RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB") { + rewardManifests = ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"]; + } else if (RewardInfo.invasionId && region.missionIndex == 0) { + // Invasion assassination has Phorid has the boss who should drop Nyx parts + // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic + rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"]; + } else { + rewardManifests = region.rewardManifests; + } let rotations: number[] = []; if (RewardInfo.jobId) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 9441f632..fbcc74fd 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -129,6 +129,8 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; + invasionId?: string; + invasionAllyFaction?: "FC_GRINEER" | "FC_CORPUS"; sortieId?: string; sortieTag?: string; sortiePrereqs?: string[]; From cdead6fdf8210e3401a02790914f4846850c9d47 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:23:52 -0700 Subject: [PATCH 535/776] feat: archon hunt rewards (#1713) also added a check for first completion to avoid giving another reward for repeating the final mission Closes #1624 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1713 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/missionInventoryUpdateController.ts | 5 +- src/models/inventoryModels/inventoryModel.ts | 12 ++- src/services/missionInventoryUpdateService.ts | 94 +++++++++++++++---- src/types/inventoryTypes/inventoryTypes.ts | 6 ++ 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 738002fd..3eb0762c 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -53,6 +53,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) logger.debug("mission report:", missionReport); const inventory = await getInventory(accountId); + const firstCompletion = missionReport.SortieId + ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 + : false; const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); if ( @@ -69,7 +72,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) } const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } = - await addMissionRewards(inventory, missionReport); + await addMissionRewards(inventory, missionReport, firstCompletion); await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index ac9edf1e..63e3842e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -91,7 +91,8 @@ import { ICrewMemberSkill, ICrewMemberSkillEfficiency, ICrewMemberDatabase, - ICrewMemberClient + ICrewMemberClient, + ISortieRewardAttenuation } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1276,6 +1277,14 @@ lastSortieRewardSchema.set("toJSON", { } }); +const sortieRewardAttenutationSchema = new Schema( + { + Tag: String, + Atten: Number + }, + { _id: false } +); + const lockedWeaponGroupSchema = new Schema( { s: Schema.Types.ObjectId, @@ -1495,6 +1504,7 @@ const inventorySchema = new Schema( CompletedSorties: [String], LastSortieReward: { type: [lastSortieRewardSchema], default: undefined }, LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined }, // Resource Extractor Drones Drones: [droneSchema], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b75522c2..7a1f61c3 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -412,7 +412,9 @@ export const addMissionInventoryUpdates = async ( break; } case "SortieId": { - inventory.CompletedSorties.push(value); + if (inventory.CompletedSorties.indexOf(value) == -1) { + inventory.CompletedSorties.push(value); + } break; } case "SeasonChallengeCompletions": { @@ -569,7 +571,8 @@ export const addMissionRewards = async ( RegularCredits: creditDrops, VoidTearParticipantsCurrWave: voidTearWave, StrippedItems: strippedItems - }: IMissionInventoryUpdateRequest + }: IMissionInventoryUpdateRequest, + firstCompletion: boolean ): Promise => { if (!rewardInfo) { //TODO: if there is a case where you can have credits collected during a mission but no rewardInfo, add credits needs to be handled earlier @@ -583,22 +586,12 @@ export const addMissionRewards = async ( } //TODO: check double reward merging - const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); + const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; const AffiliationMods: IAffiliationMods[] = []; let SyndicateXPItemReward; - if (rewardInfo.sortieTag == "Final") { - inventory.LastSortieReward = [ - { - SortieId: new Types.ObjectId(rewardInfo.sortieId!.split("_")[1]), - StoreItem: MissionRewards[0].StoreItem, - Manifest: "/Lotus/Types/Game/MissionDecks/SortieRewards" - } - ]; - } - let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display if (levelKeyName) { @@ -962,11 +955,78 @@ function getLevelCreditRewards(node: IRegion): number { //TODO: get dark sektor fixed credit rewards and railjack bonus } -function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] { +function getRandomMissionDrops( + inventory: TInventoryDatabaseDocument, + RewardInfo: IRewardInfo, + tierOverride: number | undefined, + firstCompletion: boolean +): IMissionReward[] { const drops: IMissionReward[] = []; - if (RewardInfo.sortieTag == "Final") { - const drop = getRandomRewardByChance(ExportRewards["/Lotus/Types/Game/MissionDecks/SortieRewards"][0])!; - drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + if (RewardInfo.sortieTag == "Final" && firstCompletion) { + const arr = RewardInfo.sortieId!.split("_"); + let sortieId = arr[1]; + if (sortieId == "Lite") { + sortieId = arr[2]; + + // TODO: Some way to get from sortieId to reward to make this faster + more reliable at week rollover. + const boss = getWorldState().LiteSorties[0].Boss as + | "SORTIE_BOSS_AMAR" + | "SORTIE_BOSS_NIRA" + | "SORTIE_BOSS_BOREAL"; + let crystalType = { + SORTIE_BOSS_AMAR: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", + SORTIE_BOSS_NIRA: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", + SORTIE_BOSS_BOREAL: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" + }[boss]; + const attenTag = { + SORTIE_BOSS_AMAR: "NarmerSortieAmarCrystalRewards", + SORTIE_BOSS_NIRA: "NarmerSortieNiraCrystalRewards", + SORTIE_BOSS_BOREAL: "NarmerSortieBorealCrystalRewards" + }[boss]; + const attenIndex = inventory.SortieRewardAttenuation?.findIndex(x => x.Tag == attenTag) ?? -1; + const mythicProbability = + 0.2 + (inventory.SortieRewardAttenuation?.find(x => x.Tag == attenTag)?.Atten ?? 0); + if (Math.random() < mythicProbability) { + crystalType += "Mythic"; + if (attenIndex != -1) { + inventory.SortieRewardAttenuation!.splice(attenIndex, 1); + } + } else { + if (attenIndex == -1) { + inventory.SortieRewardAttenuation ??= []; + inventory.SortieRewardAttenuation.push({ + Tag: attenTag, + Atten: 0.2 + }); + } else { + inventory.SortieRewardAttenuation![attenIndex].Atten += 0.2; + } + } + + drops.push({ StoreItem: crystalType, ItemCount: 1 }); + + const drop = getRandomRewardByChance( + ExportRewards["/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"][0] + )!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + inventory.LastLiteSortieReward = [ + { + SortieId: new Types.ObjectId(sortieId), + StoreItem: drop.type, + Manifest: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards" + } + ]; + } else { + const drop = getRandomRewardByChance(ExportRewards["/Lotus/Types/Game/MissionDecks/SortieRewards"][0])!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + inventory.LastSortieReward = [ + { + SortieId: new Types.ObjectId(sortieId), + StoreItem: drop.type, + Manifest: "/Lotus/Types/Game/MissionDecks/SortieRewards" + } + ]; + } } if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { drops.push({ diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 38ee575d..d461ef45 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -285,6 +285,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CompletedSorties: string[]; LastSortieReward?: ILastSortieRewardClient[]; LastLiteSortieReward?: ILastSortieRewardClient[]; + SortieRewardAttenuation?: ISortieRewardAttenuation[]; Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType: string; @@ -746,6 +747,11 @@ export interface ILastSortieRewardDatabase extends Omit Date: Fri, 18 Apr 2025 11:27:29 -0700 Subject: [PATCH 536/776] feat: save InvasionProgress/QualifyingInvasions (#1719) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1719 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 27 +++++++++++++++++-- src/services/missionInventoryUpdateService.ts | 20 ++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 15 ++++++++++- src/types/requestTypes.ts | 4 ++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 63e3842e..db77926e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -92,7 +92,9 @@ import { ICrewMemberSkillEfficiency, ICrewMemberDatabase, ICrewMemberClient, - ISortieRewardAttenuation + ISortieRewardAttenuation, + IInvasionProgressDatabase, + IInvasionProgressClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -684,6 +686,27 @@ questKeysSchema.set("toJSON", { export const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); +const invasionProgressSchema = new Schema( + { + invasionId: Schema.Types.ObjectId, + Delta: Number, + AttackerScore: Number, + DefenderScore: Number + }, + { _id: false } +); + +invasionProgressSchema.set("toJSON", { + transform(_doc, obj) { + const db = obj as IInvasionProgressDatabase; + const client = obj as IInvasionProgressClient; + + client._id = toOid(db.invasionId); + delete obj.invasionId; + delete obj.__v; + } +}); + const spectreLoadoutsSchema = new Schema( { ItemType: String, @@ -1482,7 +1505,7 @@ const inventorySchema = new Schema( SentientSpawnChanceBoosters: Schema.Types.Mixed, - QualifyingInvasions: [Schema.Types.Mixed], + QualifyingInvasions: [invasionProgressSchema], FactionScores: [Number], // https://warframe.fandom.com/wiki/Specter_(Tenno) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7a1f61c3..cf0b032a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -532,6 +532,26 @@ export const addMissionInventoryUpdates = async ( inventoryChanges.RegularCredits -= value; break; } + case "InvasionProgress": { + for (const clientProgress of value) { + const dbProgress = inventory.QualifyingInvasions.find(x => + x.invasionId.equals(clientProgress._id.$oid) + ); + if (dbProgress) { + dbProgress.Delta += clientProgress.Delta; + dbProgress.AttackerScore += clientProgress.AttackerScore; + dbProgress.DefenderScore += clientProgress.DefenderScore; + } else { + inventory.QualifyingInvasions.push({ + invasionId: new Types.ObjectId(clientProgress._id.$oid), + Delta: clientProgress.Delta, + AttackerScore: clientProgress.AttackerScore, + DefenderScore: clientProgress.DefenderScore + }); + } + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d461ef45..03f3fda7 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -50,6 +50,7 @@ export interface IInventoryDatabase | "LastSortieReward" | "LastLiteSortieReward" | "CrewMembers" + | "QualifyingInvasions" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -85,6 +86,7 @@ export interface IInventoryDatabase LastSortieReward?: ILastSortieRewardDatabase[]; LastLiteSortieReward?: ILastSortieRewardDatabase[]; CrewMembers: ICrewMemberDatabase[]; + QualifyingInvasions: IInvasionProgressDatabase[]; } export interface IQuestKeyDatabase { @@ -272,7 +274,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; SupportedSyndicate?: string; Affiliations: IAffiliation[]; - QualifyingInvasions: any[]; + QualifyingInvasions: IInvasionProgressClient[]; FactionScores: number[]; ArchwingEnabled?: boolean; PendingSpectreLoadouts?: ISpectreLoadout[]; @@ -676,6 +678,17 @@ export interface IInvasionChainProgress { count: number; } +export interface IInvasionProgressClient { + _id: IOid; + Delta: number; + AttackerScore: number; + DefenderScore: number; +} + +export interface IInvasionProgressDatabase extends Omit { + invasionId: Types.ObjectId; +} + export interface IKubrowPetEggClient { ItemType: string; ExpirationDate: IMongoDate; // seems to be set to 7 days ahead @ 0 UTC diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index fbcc74fd..b81fa064 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -19,7 +19,8 @@ import { ICollectibleEntry, IDiscoveredMarker, ILockedWeaponGroupClient, - ILoadOutPresets + ILoadOutPresets, + IInvasionProgressClient } from "./inventoryTypes/inventoryTypes"; import { IGroup } from "./loginTypes"; @@ -123,6 +124,7 @@ export type IMissionInventoryUpdateRequest = { }; wagerTier?: number; // the index creditsFee?: number; // the index + InvasionProgress?: IInvasionProgressClient[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From 37ac10acd2b70f0f6be19456e72b192db3c96868 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:03:53 -0700 Subject: [PATCH 537/776] chore: use import for static vendor manifest json files again (#1725) This was changed because for VRST_WEAPON, the LocTagRandSeed is too big to be read without precision loss, but both vendors using it are now auto-generated, so we can have hot-reloading again when these files are changed. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1725 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 96 +++++++++++++++--------- src/types/vendorTypes.ts | 2 +- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index f6007642..3abe56fa 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,6 +1,4 @@ -import fs from "fs"; -import path from "path"; -import { repoDir } from "@/src/helpers/pathHelper"; +import { unixTimesInMs } from "@/src/constants/timeConstants"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { @@ -9,44 +7,68 @@ import { IVendorInfo, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; -import { JSONParse } from "json-with-bigint"; import { ExportVendors } from "warframe-public-export-plus"; -import { unixTimesInMs } from "../constants/timeConstants"; -const getVendorManifestJson = (name: string): IRawVendorManifest => { - return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); -}; +import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; +import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; +import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json"; +import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json"; +import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json"; +import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json"; +import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json"; +import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json"; +import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json"; +import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json"; +import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json"; +import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; +import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; +import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; +import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json"; +import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; +import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; +import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; +import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; +import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; +import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; +import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; +import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; +import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json"; +import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; +import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; +import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; +import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; +import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; const rawVendorManifests: IRawVendorManifest[] = [ - getVendorManifestJson("ArchimedeanVendorManifest"), - getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"), - getVendorManifestJson("DeimosFishmongerVendorManifest"), - getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"), - getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"), - getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"), - getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"), - getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"), - getVendorManifestJson("DeimosHivemindTokenVendorManifest"), - getVendorManifestJson("DeimosPetVendorManifest"), - getVendorManifestJson("DeimosProspectorVendorManifest"), - getVendorManifestJson("DuviriAcrithisVendorManifest"), - getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"), - getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), - getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing - getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing - getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), - getVendorManifestJson("MaskSalesmanManifest"), - getVendorManifestJson("Nova1999ConquestShopManifest"), - getVendorManifestJson("OstronFishmongerVendorManifest"), - getVendorManifestJson("OstronPetVendorManifest"), - getVendorManifestJson("OstronProspectorVendorManifest"), - getVendorManifestJson("RadioLegionIntermission12VendorManifest"), - getVendorManifestJson("SolarisDebtTokenVendorManifest"), - getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"), - getVendorManifestJson("SolarisFishmongerVendorManifest"), - getVendorManifestJson("SolarisProspectorVendorManifest"), - getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing - getVendorManifestJson("ZarimanCommisionsManifestArchimedean") + ArchimedeanVendorManifest, + DeimosEntratiFragmentVendorProductsManifest, + DeimosFishmongerVendorManifest, + DeimosHivemindCommisionsManifestFishmonger, + DeimosHivemindCommisionsManifestPetVendor, + DeimosHivemindCommisionsManifestProspector, + DeimosHivemindCommisionsManifestTokenVendor, + DeimosHivemindCommisionsManifestWeaponsmith, + DeimosHivemindTokenVendorManifest, + DeimosPetVendorManifest, + DeimosProspectorVendorManifest, + DuviriAcrithisVendorManifest, + EntratiLabsEntratiLabsCommisionsManifest, + EntratiLabsEntratiLabVendorManifest, + GuildAdvertisementVendorManifest, // uses preprocessing + HubsIronwakeDondaVendorManifest, // uses preprocessing + HubsRailjackCrewMemberVendorManifest, + MaskSalesmanManifest, + Nova1999ConquestShopManifest, + OstronFishmongerVendorManifest, + OstronPetVendorManifest, + OstronProspectorVendorManifest, + RadioLegionIntermission12VendorManifest, + SolarisDebtTokenVendorManifest, + SolarisDebtTokenVendorRepossessionsManifest, + SolarisFishmongerVendorManifest, + SolarisProspectorVendorManifest, + TeshinHardModeVendorManifest, // uses preprocessing + ZarimanCommisionsManifestArchimedean ]; interface IGeneratableVendorInfo extends Omit { diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index 2967a1e5..f14d3f55 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -32,7 +32,7 @@ export interface IVendorInfo { TypeName: string; ItemManifest: IItemManifest[]; PropertyTextHash?: string; - RandomSeedType?: "VRST_WEAPON"; + RandomSeedType?: string; RequiredGoalTag?: string; WeaponUpgradeValueAttenuationExponent?: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. From 5eecf11b1a10d52d4d2acd9c8647c1a6cacad8f2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:04:04 -0700 Subject: [PATCH 538/776] fix: ignore assassin mission failure if recovery is still pending (#1726) Closes #1724 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1726 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index cf0b032a..a7308aa6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -132,11 +132,13 @@ export const addMissionInventoryUpdates = async ( // Somewhat heuristically detect G3 capture: // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694 + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1724 if ( inventoryUpdates.MissionFailed && inventoryUpdates.MissionStatus == "GS_FAILURE" && inventoryUpdates.ObjectiveReached && !inventoryUpdates.LockedWeaponGroup && + !inventory.LockedWeaponGroup && !inventoryUpdates.LevelKeyName ) { const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; From 8afb5152315a4a78c69587242b7dced3b68a6440 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:04:22 -0700 Subject: [PATCH 539/776] fix(stats): captures not being tracked for a new enemy (#1728) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1728 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/statsService.ts | 47 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 626bbe27..a074b637 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -1,6 +1,5 @@ import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; import { - IEnemy, IStatsAdd, IStatsMax, IStatsSet, @@ -137,34 +136,34 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) case "HEADSHOT": case "KILL_ASSIST": { playerStats.Enemies ??= []; - const enemyStatKey = { - KILL_ENEMY: "kills", - EXECUTE_ENEMY: "executions", - HEADSHOT: "headshots", - KILL_ASSIST: "assists" - }[category] as "kills" | "executions" | "headshots" | "assists"; + const enemyStatKey = ( + { + KILL_ENEMY: "kills", + EXECUTE_ENEMY: "executions", + HEADSHOT: "headshots", + KILL_ASSIST: "assists" + } as const + )[category]; for (const [type, count] of Object.entries(data as IUploadEntry)) { - const enemy = playerStats.Enemies.find(element => element.type === type); - if (enemy) { - if (category === "KILL_ENEMY") { - enemy.kills ??= 0; - const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type]; - if (captureCount) { - enemy.kills += Math.max(count - captureCount, 0); - enemy.captures ??= 0; - enemy.captures += captureCount; - } else { - enemy.kills += count; - } + let enemy = playerStats.Enemies.find(element => element.type === type); + if (!enemy) { + enemy = { type: type }; + playerStats.Enemies.push(enemy); + } + if (category === "KILL_ENEMY") { + enemy.kills ??= 0; + const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type]; + if (captureCount) { + enemy.kills += Math.max(count - captureCount, 0); + enemy.captures ??= 0; + enemy.captures += captureCount; } else { - enemy[enemyStatKey] ??= 0; - enemy[enemyStatKey] += count; + enemy.kills += count; } } else { - const newEnemy: IEnemy = { type: type }; - newEnemy[enemyStatKey] = count; - playerStats.Enemies.push(newEnemy); + enemy[enemyStatKey] ??= 0; + enemy[enemyStatKey] += count; } } break; From c1ca303310ab8eb1716fdbdd8ec245a84adfadd6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:05:23 -0700 Subject: [PATCH 540/776] fix: handle mk1 armaments being salvaged (#1730) Fixes #1729 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1730 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts index 6cde1cda..4ab7c466 100644 --- a/src/controllers/api/crewShipIdentifySalvageController.ts +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -12,6 +12,7 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getRandomInt } from "@/src/services/rngService"; import { IFingerprintStat } from "@/src/helpers/rivenHelper"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -42,22 +43,33 @@ export const crewShipIdentifySalvageController: RequestHandler = async (req, res ); } else { const meta = ExportRailjackWeapons[payload.ItemType]; - const upgradeType = meta.defaultUpgrades![0].ItemType; - const upgradeMeta = ExportUpgrades[upgradeType]; - const buffs: IFingerprintStat[] = []; - for (const buff of upgradeMeta.upgradeEntries!) { - buffs.push({ - Tag: buff.tag, - Value: Math.trunc(Math.random() * 0x40000000) - }); + let defaultOverwrites: Partial | undefined; + if (meta.defaultUpgrades?.[0]) { + const upgradeType = meta.defaultUpgrades[0].ItemType; + const upgradeMeta = ExportUpgrades[upgradeType]; + const buffs: IFingerprintStat[] = []; + for (const buff of upgradeMeta.upgradeEntries!) { + buffs.push({ + Tag: buff.tag, + Value: Math.trunc(Math.random() * 0x40000000) + }); + } + defaultOverwrites = { + UpgradeType: upgradeType, + UpgradeFingerprint: JSON.stringify({ + compat: payload.ItemType, + buffs + } satisfies IInnateDamageFingerprint) + }; } - addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, undefined, inventoryChanges, { - UpgradeType: upgradeType, - UpgradeFingerprint: JSON.stringify({ - compat: payload.ItemType, - buffs - } satisfies IInnateDamageFingerprint) - }); + addEquipment( + inventory, + "CrewShipSalvagedWeapons", + payload.ItemType, + undefined, + inventoryChanges, + defaultOverwrites + ); } inventoryChanges.CrewShipRawSalvage = [ From e59bdcdfbc25a0148129faa094b4aa05604fe909 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:05:43 -0700 Subject: [PATCH 541/776] chore(webui): assume deleting items will always succeed (#1731) instead of waiting for a response + then refreshing inventory, we can just delete the element right away and hope it works out Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1731 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index e8e4821e..f8fe085c 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -487,6 +487,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById(category + "-list").removeChild(tr); disposeOfGear(category, item.ItemId.$oid); }; a.title = loc("code_remove"); @@ -683,6 +684,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById("riven-list").removeChild(tr); disposeOfGear("Upgrades", item.ItemId.$oid); }; a.title = loc("code_remove"); @@ -723,6 +725,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById("mods-list").removeChild(tr); disposeOfGear("Upgrades", item.ItemId.$oid); }; a.title = loc("code_remove"); @@ -765,6 +768,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById("mods-list").removeChild(tr); disposeOfItems("Upgrades", item.ItemType, item.ItemCount); }; a.title = loc("code_remove"); @@ -1097,8 +1101,6 @@ function disposeOfGear(category, oid) { url: "/api/sell.php?" + window.authz, contentType: "text/plain", data: JSON.stringify(data) - }).done(function () { - updateInventory(); }); }); } @@ -1120,8 +1122,6 @@ function disposeOfItems(category, type, count) { url: "/api/sell.php?" + window.authz, contentType: "text/plain", data: JSON.stringify(data) - }).done(function () { - updateInventory(); }); }); } From 26f37f58e555e50b5104058c55fa8c17ff03b924 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:05:55 -0700 Subject: [PATCH 542/776] chore(webui): make add mods behave more like adding items (#1732) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1732 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index f8fe085c..ed71ed96 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1260,21 +1260,28 @@ function doAcquireMod() { $("#mod-to-acquire").addClass("is-invalid").focus(); return; } - revalidateAuthz(() => { - $.post({ - url: "/custom/addItems?" + window.authz, - contentType: "application/json", - data: JSON.stringify([ - { - ItemType: uniqueName, - ItemCount: parseInt($("#mod-count").val()) + const count = parseInt($("#mod-count").val()); + if (count != 0) { + revalidateAuthz(() => { + $.post({ + url: "/custom/addItems?" + window.authz, + contentType: "application/json", + data: JSON.stringify([ + { + ItemType: uniqueName, + ItemCount: count + } + ]) + }).done(function () { + if (count > 0) { + toast(loc("code_succAdded")); + } else { + toast(loc("code_succRemoved")); } - ]) - }).done(function () { - document.getElementById("mod-to-acquire").value = ""; - updateInventory(); + updateInventory(); + }); }); - }); + } } const uiConfigs = [...$("#server-settings input[id]")].map(x => x.id); From ba1380ec4cd5ded6f3d9c2f15a9396b830106ac7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:07 -0700 Subject: [PATCH 543/776] feat: rush repair drones (#1733) Closes #1677 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1733 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index b2490178..b1612be9 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -360,6 +360,22 @@ export const guildTechController: RequestHandler = async (req, res) => { res.json({ inventoryChanges: inventoryChanges }); + } else if (data.Action == "InstantFinish") { + if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") { + throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); + } + const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!); + inventoryChanges.MiscItems = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem", + ItemCount: -1 + } + ]; + addMiscItems(inventory, inventoryChanges.MiscItems); + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); } else { logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unhandled guildTech request`); @@ -372,7 +388,7 @@ type TGuildTechRequest = | IGuildTechContributeRequest; interface IGuildTechBasicRequest { - Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush"; + Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish"; Mode: "Guild" | "Personal"; RecipeType: string; TechProductCategory?: string; @@ -406,11 +422,19 @@ const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: s inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins"; + return finishComponentRepair(inventory, category, itemId); +}; + +const finishComponentRepair = ( + inventory: TInventoryDatabaseDocument, + category: "CrewShipWeapons" | "CrewShipWeaponSkins", + itemId: string +): IInventoryChanges => { const salvageCategory = getSalvageCategory(category); // find salved part & delete it const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId)); - const salvageItem = inventory[category][salvageIndex]; + const salvageItem = inventory[salvageCategory][salvageIndex]; inventory[salvageCategory].splice(salvageIndex, 1); // add final item From 7040d422a27f596dfc25902a5ae2be1c6e8a3492 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:20 -0700 Subject: [PATCH 544/776] feat: manage crew members (#1734) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1734 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/crewMembersController.ts | 28 ++++++++++++++++++++ src/routes/api.ts | 2 ++ src/types/inventoryTypes/inventoryTypes.ts | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/crewMembersController.ts diff --git a/src/controllers/api/crewMembersController.ts b/src/controllers/api/crewMembersController.ts new file mode 100644 index 00000000..15ef0fbf --- /dev/null +++ b/src/controllers/api/crewMembersController.ts @@ -0,0 +1,28 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const crewMembersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "CrewMembers"); + const data = getJSONfromString(String(req.body)); + const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!; + dbCrewMember.AssignedRole = data.crewMember.AssignedRole; + dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency; + dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx; + dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid); + dbCrewMember.Configs = data.crewMember.Configs; + dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand; + await inventory.save(); + res.json({ + crewMemberId: data.crewMember.ItemId.$oid, + NemesisFingerprint: data.crewMember.NemesisFingerprint + }); +}; + +interface ICrewMembersRequest { + crewMember: ICrewMemberClient; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 71b90dc4..e6416f7a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -28,6 +28,7 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { crewMembersController } from "@/src/controllers/api/crewMembersController"; import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; @@ -222,6 +223,7 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/crewMembers.php", crewMembersController); apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 03f3fda7..d5f6344a 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -489,7 +489,7 @@ export interface ICrewMemberClient { XP: number; PowersuitType: string; Configs: IItemConfig[]; - SecondInCommand: boolean; + SecondInCommand: boolean; // on call ItemId: IOid; } From c2a633b549e21b2c56c43b8f0fb4821882402152 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:38 -0700 Subject: [PATCH 545/776] chore: improve LiteSortie handling at week rollover (#1735) WorldState now provides the upcoming LiteSortie if relevant and the boss is derived from the sortieId so completing it at rollover should work as expected. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1735 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 8 +- src/services/worldStateService.ts | 138 ++++++++++-------- src/types/worldStateTypes.ts | 2 +- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a7308aa6..7b27faa9 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -53,7 +53,7 @@ import conservationAnimals from "@/static/fixed_responses/conservationAnimals.js import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -import { getWorldState } from "./worldStateService"; +import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; import { config } from "./configService"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { @@ -990,11 +990,7 @@ function getRandomMissionDrops( if (sortieId == "Lite") { sortieId = arr[2]; - // TODO: Some way to get from sortieId to reward to make this faster + more reliable at week rollover. - const boss = getWorldState().LiteSorties[0].Boss as - | "SORTIE_BOSS_AMAR" - | "SORTIE_BOSS_NIRA" - | "SORTIE_BOSS_BOREAL"; + const boss = getLiteSortie(idToWeek(sortieId)).Boss; let crystalType = { SORTIE_BOSS_AMAR: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", SORTIE_BOSS_NIRA: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index ed65a8fc..6083dcb5 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -4,7 +4,14 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { ICalendarDay, ICalendarSeason, ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes"; +import { + ICalendarDay, + ICalendarSeason, + ILiteSortie, + ISeasonChallenge, + ISortie, + IWorldState +} from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -941,65 +948,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { pushSortieIfRelevant(worldState.Sorties, day); // Archon Hunt cycling every week - // TODO: Handle imminent rollover - { - const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; - const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; - const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth - - const nodes: string[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - value.systemIndex === systemIndex && - value.factionIndex !== undefined && - value.factionIndex < 2 && - value.name.indexOf("Archwing") == -1 && - value.missionIndex != 0 // Exclude MT_ASSASSINATION - ) { - nodes.push(key); - } - } - - const rng = new CRng(week); - const firstNodeIndex = rng.randomInt(0, nodes.length - 1); - const firstNode = nodes[firstNodeIndex]; - nodes.splice(firstNodeIndex, 1); - worldState.LiteSorties.push({ - _id: { - $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" - }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", - Seed: week, - Boss: boss, - Missions: [ - { - missionType: rng.randomElement([ - "MT_INTEL", - "MT_MOBILE_DEFENSE", - "MT_EXTERMINATION", - "MT_SABOTAGE", - "MT_RESCUE" - ]), - node: firstNode - }, - { - missionType: rng.randomElement([ - "MT_DEFENSE", - "MT_TERRITORY", - "MT_ARTIFACT", - "MT_EXCAVATE", - "MT_SURVIVAL" - ]), - node: rng.randomElement(nodes) - }, - { - missionType: "MT_ASSASSINATION", - node: showdownNode - } - ] - }); + worldState.LiteSorties.push(getLiteSortie(week)); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + worldState.LiteSorties.push(getLiteSortie(week + 1)); } // Circuit choices cycling every week @@ -1071,3 +1022,70 @@ export const getWorldState = (buildLabel?: string): IWorldState => { return worldState; }; + +export const idToWeek = (id: string): number => { + return (parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800000; +}; + +export const getLiteSortie = (week: number): ILiteSortie => { + const boss = (["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"] as const)[week % 3]; + const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; + const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth + + const nodes: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.factionIndex !== undefined && + value.factionIndex < 2 && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 // Exclude MT_ASSASSINATION + ) { + nodes.push(key); + } + } + + const rng = new CRng(week); + const firstNodeIndex = rng.randomInt(0, nodes.length - 1); + const firstNode = nodes[firstNodeIndex]; + nodes.splice(firstNodeIndex, 1); + + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + return { + _id: { + $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", + Seed: week, + Boss: boss, + Missions: [ + { + missionType: rng.randomElement([ + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_EXTERMINATION", + "MT_SABOTAGE", + "MT_RESCUE" + ]), + node: firstNode + }, + { + missionType: rng.randomElement([ + "MT_DEFENSE", + "MT_TERRITORY", + "MT_ARTIFACT", + "MT_EXCAVATE", + "MT_SURVIVAL" + ]), + node: rng.randomElement(nodes) + }, + { + missionType: "MT_ASSASSINATION", + node: showdownNode + } + ] + }; +}; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 278f6e95..67178c73 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -103,7 +103,7 @@ export interface ILiteSortie { Expiry: IMongoDate; Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; Seed: number; - Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL" + Boss: "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL"; Missions: { missionType: string; node: string; From 0f3d9f6c2cb1bbec0a365c9770d18cc5a6ad2688 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:49 -0700 Subject: [PATCH 546/776] chore: provide upcoming weekly acts before week rollover (#1736) The final piece to close #1640 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1736 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 55 ++++++++++++++++++------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 6083dcb5..d8cd0c3c 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -355,6 +355,34 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; +const pushWeeklyActs = (worldState: IWorldState, week: number): void => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + }); +}; + const birthdays: number[] = [ 1, // Kaya 45, // Lettie @@ -611,29 +639,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); } - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); - worldState.SeasonInfo.ActiveChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) - }); - worldState.SeasonInfo.ActiveChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) - }); - worldState.SeasonInfo.ActiveChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) - }); - // TODO: Provide upcoming weekly acts if rollover is imminent + pushWeeklyActs(worldState, week); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + pushWeeklyActs(worldState, week + 1); + } // Elite Sanctuary Onslaught cycling every week worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful From 8fd7152c41e71b269c880b0d4407aae1a42d7ac8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 07:53:11 -0700 Subject: [PATCH 547/776] fix: don't give rewards for aborted railjack missions (#1743) Fixes #1741 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1743 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7b27faa9..4dcb0fe4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -71,7 +71,12 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] return [rewardInfo.rewardTier]; } - const rotationCount = rewardInfo.rewardQualifications?.length || 0; + // Aborting a railjack mission should not give any rewards (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) + if (rewardInfo.rewardQualifications === undefined) { + return []; + } + + const rotationCount = rewardInfo.rewardQualifications.length || 0; if (rotationCount === 0) return [0]; const rotationPattern = From 11f2ffe64ddc57fea20677671eb6a55232c1fe7f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 07:53:45 -0700 Subject: [PATCH 548/776] feat(import): accolades (#1750) So one is able to import e.g. `{"Staff":true}` to set that field to true without going into Compass. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1750 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 20 +++++++++++++++++++- src/services/importService.ts | 17 +++++++++++++++-- src/types/inventoryTypes/inventoryTypes.ts | 8 +++++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index db77926e..029bf6c0 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -94,7 +94,8 @@ import { ICrewMemberClient, ISortieRewardAttenuation, IInvasionProgressDatabase, - IInvasionProgressClient + IInvasionProgressClient, + IAccolades } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1059,6 +1060,13 @@ pendingRecipeSchema.set("toJSON", { } }); +const accoladesSchema = new Schema( + { + Heirloom: Boolean + }, + { _id: false } +); + const infestedFoundrySchema = new Schema( { Name: String, @@ -1466,6 +1474,16 @@ const inventorySchema = new Schema( //Mastery Rank next availability TrainingDate: { type: Date, default: new Date(0) }, + //Accolades + Staff: Boolean, + Founder: Number, + Guide: Number, + Moderator: Boolean, + Partner: Boolean, + Accolades: accoladesSchema, + //Not an accolade but unlocks an extra chat + Counselor: Boolean, + //you saw last played Region when you opened the star map LastRegionPlayed: String, diff --git a/src/services/importService.ts b/src/services/importService.ts index 0f5dd28d..ea03047b 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -230,17 +230,23 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< replaceSlots(db[key], client[key]); } } + // boolean for (const key of [ "UseAdultOperatorLoadout", "HasOwnedVoidProjectionsPreviously", "ReceivedStartingGear", "ArchwingEnabled", - "PlayedParkourTutorial" + "PlayedParkourTutorial", + "Staff", + "Moderator", + "Partner", + "Counselor" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } + // number for (const key of [ "PlayerLevel", "RegularCredits", @@ -250,12 +256,15 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "PrimeTokens", "TradesRemaining", "GiftsRemaining", - "ChallengesFixVersion" + "ChallengesFixVersion", + "Founder", + "Guide" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } + // string for (const key of [ "ThemeStyle", "ThemeBackground", @@ -270,6 +279,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< db[key] = client[key]; } } + // string[] for (const key of [ "EquippedGear", "EquippedEmotes", @@ -380,6 +390,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< }); }); } + if (client.Accolades !== undefined) { + db.Accolades = client.Accolades; + } }; const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d5f6344a..b4dbc57c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -250,9 +250,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Guide?: number; Moderator?: boolean; Partner?: boolean; - Accolades?: { - Heirloom?: boolean; - }; + Accolades?: IAccolades; Counselor?: boolean; Upgrades: IUpgradeClient[]; EquippedGear: string[]; @@ -914,6 +912,10 @@ export interface IPendingRecipeClient CompletionDate: IMongoDate; } +export interface IAccolades { + Heirloom?: boolean; +} + export interface IPendingTrade { State: number; SelfReady: boolean; From 86d871537bd54ed48756641e3a6503bf63c482e3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:18:00 +0200 Subject: [PATCH 549/776] chore: add void corrupted moa to allScans.json --- static/fixed_responses/allScans.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index f4b43062..64eebe52 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -1097,5 +1097,6 @@ "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", - "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem" + "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem", + "/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar" ] From 218df461e1e29e1345642fc75a205dfbeb10606a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 16:10:45 -0700 Subject: [PATCH 550/776] feat: send WiTW email when completing The New War or Heart of Deimos (#1749) At completion of either of the quests, check if the other has been completed, and if so, unlock WiTW. Closes #1748 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1749 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/questService.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/services/questService.ts b/src/services/questService.ts index 85cefa4b..d5ac28dc 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -216,6 +216,27 @@ const handleQuestCompletion = async ( setupKahlSyndicate(inventory); } + // Whispers in the Walls is unlocked once The New + Heart of Deimos are completed. + if ( + (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && + inventory.QuestKeys.find( + x => x.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" + )?.Completed) || + (questKey == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" && + inventory.QuestKeys.find(x => x.ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain")?.Completed) + ) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Loid", + msg: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxBody", + att: ["/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain"], + sub: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", + highPriority: true + } + ]); + } + const questCompletionItems = getQuestCompletionItems(questKey); logger.debug(`quest completion items`, questCompletionItems); if (questCompletionItems) { From 98975edca1b04bdebd1a9b90adbdb139c2ad1f4f Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:42:48 -0700 Subject: [PATCH 551/776] feat(webui): KubrowPets support (#1752) also using `/api/modularWeaponCrafting.php` instead of `/custom/addModularEquipment` for modular equipment Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1752 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../api/modularWeaponCraftingController.ts | 45 ++-- src/controllers/api/releasePetController.ts | 8 +- .../custom/addModularEquipmentController.ts | 98 ------- .../custom/getItemListsController.ts | 15 +- src/helpers/inventoryHelpers.ts | 142 ++++++++++ src/routes/custom.ts | 2 - src/services/inventoryService.ts | 104 ++++++- static/webui/index.html | 29 +- static/webui/script.js | 253 ++++++++++++++---- static/webui/translations/de.js | 3 + static/webui/translations/en.js | 3 + static/webui/translations/es.js | 3 + static/webui/translations/fr.js | 3 + static/webui/translations/ru.js | 3 + static/webui/translations/zh.js | 3 + 15 files changed, 538 insertions(+), 176 deletions(-) delete mode 100644 src/controllers/custom/addModularEquipmentController.ts diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 8819a2a6..0d083371 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -23,6 +23,7 @@ import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { WeaponType: string; Parts: string[]; + isWebUi?: boolean; } export const modularWeaponCraftingController: RequestHandler = async (req, res) => { @@ -139,33 +140,39 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) } defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); - combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false)); + combineInventoryChanges( + inventoryChanges, + occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi) + ); if (defaultUpgrades) { inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); } // Remove credits & parts const miscItemChanges = []; - for (const part of data.Parts) { - miscItemChanges.push({ - ItemType: part, - ItemCount: -1 - }); + let currencyChanges = {}; + if (!data.isWebUi) { + for (const part of data.Parts) { + miscItemChanges.push({ + ItemType: part, + ItemCount: -1 + }); + } + currencyChanges = updateCurrency( + inventory, + category == "Hoverboards" || + category == "MoaPets" || + category == "LongGuns" || + category == "Pistols" || + category == "KubrowPets" + ? 5000 + : 4000, // Definitely correct for Melee & OperatorAmps + false + ); + addMiscItems(inventory, miscItemChanges); } - const currencyChanges = updateCurrency( - inventory, - category == "Hoverboards" || - category == "MoaPets" || - category == "LongGuns" || - category == "Pistols" || - category == "KubrowPets" - ? 5000 - : 4000, // Definitely correct for Melee & OperatorAmps - false - ); - addMiscItems(inventory, miscItemChanges); - await inventory.save(); + await inventory.save(); // Tell client what we did res.json({ InventoryChanges: { diff --git a/src/controllers/api/releasePetController.ts b/src/controllers/api/releasePetController.ts index 10625778..5e1d792b 100644 --- a/src/controllers/api/releasePetController.ts +++ b/src/controllers/api/releasePetController.ts @@ -8,7 +8,11 @@ export const releasePetController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId, "RegularCredits KubrowPets"); const payload = getJSONfromString(String(req.body)); - const inventoryChanges = updateCurrency(inventory, 25000, false); + const inventoryChanges = updateCurrency( + inventory, + payload.recipeName == "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" ? 25000 : 0, + false + ); inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }]; inventory.KubrowPets.pull({ _id: payload.petId }); @@ -18,6 +22,6 @@ export const releasePetController: RequestHandler = async (req, res) => { }; interface IReleasePetRequest { - recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe"; + recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" | "webui"; petId: string; } diff --git a/src/controllers/custom/addModularEquipmentController.ts b/src/controllers/custom/addModularEquipmentController.ts deleted file mode 100644 index 984acd75..00000000 --- a/src/controllers/custom/addModularEquipmentController.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { - getInventory, - addEquipment, - occupySlot, - productCategoryToInventoryBin, - applyDefaultUpgrades -} from "@/src/services/inventoryService"; -import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; -import { getDefaultUpgrades } from "@/src/services/itemDataService"; -import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { ExportWeapons } from "warframe-public-export-plus"; -import { RequestHandler } from "express"; - -export const addModularEquipmentController: RequestHandler = async (req, res) => { - const requiredFields = new Set(); - const accountId = await getAccountIdForRequest(req); - const request = req.body as IAddModularEquipmentRequest; - const category = modularWeaponTypes[request.ItemType]; - const inventoryBin = productCategoryToInventoryBin(category)!; - requiredFields.add(category); - requiredFields.add(inventoryBin); - - request.ModularParts.forEach(part => { - if (ExportWeapons[part].gunType) { - if (category == "LongGuns") { - request.ItemType = { - GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", - GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", - GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" - }[ExportWeapons[part].gunType]; - } else { - request.ItemType = { - GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", - GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", - GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" - }[ExportWeapons[part].gunType]; - } - } else if (request.ItemType == "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") { - if (part.includes("ZanukaPetPartHead")) { - request.ItemType = { - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" - }[part]!; - } - } - }); - const defaultUpgrades = getDefaultUpgrades(request.ModularParts); - if (defaultUpgrades) { - requiredFields.add("RawUpgrades"); - } - const defaultWeaponsMap: Record = { - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": [ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIP" - ], - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": [ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIS" - ], - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": [ - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS" - ] - }; - const defaultWeapons = defaultWeaponsMap[request.ItemType] as string[] | undefined; - if (defaultWeapons) { - for (const defaultWeapon of defaultWeapons) { - const category = ExportWeapons[defaultWeapon].productCategory; - requiredFields.add(category); - requiredFields.add(productCategoryToInventoryBin(category)); - } - } - - const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); - if (defaultWeapons) { - for (const defaultWeapon of defaultWeapons) { - const category = ExportWeapons[defaultWeapon].productCategory; - addEquipment(inventory, category, defaultWeapon); - occupySlot(inventory, productCategoryToInventoryBin(category)!, true); - } - } - - const defaultOverwrites: Partial = { - Configs: applyDefaultUpgrades(inventory, defaultUpgrades) - }; - - addEquipment(inventory, category, request.ItemType, request.ModularParts, undefined, defaultOverwrites); - occupySlot(inventory, inventoryBin, true); - await inventory.save(); - res.end(); -}; - -interface IAddModularEquipmentRequest { - ItemType: string; - ModularParts: string[]; -} diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 38e42bdb..214c51e7 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -56,6 +56,7 @@ const getItemListsController: RequestHandler = (req, response) => { res.Syndicates = []; res.OperatorAmps = []; res.QuestKeys = []; + res.KubrowPets = []; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ uniqueName, @@ -64,7 +65,7 @@ const getItemListsController: RequestHandler = (req, response) => { }); } for (const [uniqueName, item] of Object.entries(ExportSentinels)) { - if (item.productCategory == "Sentinels") { + if (item.productCategory != "SpecialItems") { res[item.productCategory].push({ uniqueName, name: getString(item.name, lang) @@ -73,11 +74,13 @@ const getItemListsController: RequestHandler = (req, response) => { } for (const [uniqueName, item] of Object.entries(ExportWeapons)) { if (item.partType) { - res.ModularParts.push({ - uniqueName, - name: getString(item.name, lang), - partType: item.partType - }); + if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) { + res.ModularParts.push({ + uniqueName, + name: getString(item.name, lang), + partType: item.partType + }); + } if (uniqueName.split("/")[5] != "SentTrainingAmplifier") { res.miscitems.push({ uniqueName: uniqueName, diff --git a/src/helpers/inventoryHelpers.ts b/src/helpers/inventoryHelpers.ts index d7fa7b25..79466fb3 100644 --- a/src/helpers/inventoryHelpers.ts +++ b/src/helpers/inventoryHelpers.ts @@ -1,5 +1,6 @@ import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { Types } from "mongoose"; +import { TRarity } from "warframe-public-export-plus"; export const toOid = (objectId: Types.ObjectId): IOid => { return { $oid: objectId.toString() } satisfies IOid; @@ -8,3 +9,144 @@ export const toOid = (objectId: Types.ObjectId): IOid => { export const toMongoDate = (date: Date): IMongoDate => { return { $date: { $numberLong: date.getTime().toString() } }; }; + +export const kubrowWeights: Record = { + COMMON: 6, + UNCOMMON: 4, + RARE: 2, + LEGENDARY: 1 +}; + +export const kubrowFurPatternsWeights: Record = { + COMMON: 6, + UNCOMMON: 5, + RARE: 2, + LEGENDARY: 1 +}; + +export const catbrowDetails = { + Colors: [ + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" as TRarity }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" as TRarity }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" as TRarity }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" as TRarity } + ], + + EyeColors: [ + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" as TRarity } + ], + + FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" as TRarity }], + + BodyTypes: [ + { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" as TRarity } + ], + + Heads: [ + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" as TRarity } + ], + + Tails: [ + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" as TRarity } + ] +}; + +export const kubrowDetails = { + Colors: [ + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" as TRarity } + ], + + EyeColors: [ + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" as TRarity } + ], + + FurPatterns: [ + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" as TRarity } + ], + + BodyTypes: [ + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" as TRarity } + ], + + Heads: [], + + Tails: [] +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 16359226..fbd859c1 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -15,7 +15,6 @@ import { createAccountController } from "@/src/controllers/custom/createAccountC import { createMessageController } from "@/src/controllers/custom/createMessageController"; import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController"; import { addItemsController } from "@/src/controllers/custom/addItemsController"; -import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController"; import { addXpController } from "@/src/controllers/custom/addXpController"; import { importController } from "@/src/controllers/custom/importController"; @@ -40,7 +39,6 @@ customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); customRouter.post("/addCurrency", addCurrencyController); customRouter.post("/addItems", addItemsController); -customRouter.post("/addModularEquipment", addModularEquipmentController); customRouter.post("/addXp", addXpController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 7537c8d6..fd6a597b 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -23,7 +23,10 @@ import { IUpgradeClient, TPartialStartingGear, ILoreFragmentScan, - ICrewMemberClient + ICrewMemberClient, + Status, + IKubrowPetDetailsDatabase, + ITraits } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -58,14 +61,21 @@ import { ExportWeapons, IDefaultUpgrade, IPowersuit, + ISentinel, TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; -import { toOid } from "../helpers/inventoryHelpers"; +import { + catbrowDetails, + kubrowDetails, + kubrowFurPatternsWeights, + kubrowWeights, + toOid +} from "../helpers/inventoryHelpers"; import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; -import { getRandomElement, getRandomInt, SRng } from "./rngService"; +import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService"; import { createMessage } from "./inboxService"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; @@ -714,6 +724,11 @@ export const addItem = async ( return { MiscItems: miscItemChanges }; + } else if ( + typeName.substr(1).split("/")[3] == "CatbrowPet" || + typeName.substr(1).split("/")[3] == "KubrowPet" + ) { + return addKubrowPet(inventory, typeName, undefined, premiumPurchase); } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { if (!seed) { throw new Error(`Expected crew member to have a seed`); @@ -932,6 +947,89 @@ export const addSpaceSuit = ( return inventoryChanges; }; +export const addKubrowPet = ( + inventory: TInventoryDatabaseDocument, + kubrowPetName: string, + details: IKubrowPetDetailsDatabase | undefined, + premiumPurchase: boolean, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + + const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined; + const exalted = kubrowPet?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const configs: IItemConfig[] = applyDefaultUpgrades(inventory, kubrowPet?.defaultUpgrades); + + if (!details) { + let traits: ITraits; + + if (kubrowPetName == "/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit") { + traits = { + BaseColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseVampire", + SecondaryColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryVampire", + TertiaryColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryVampire", + AccentColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsVampire", + EyeColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", + FurPattern: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternVampire", + Personality: kubrowPetName, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetVampireBodyType", + Head: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadVampire", + Tail: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailVampire" + }; + } else { + const isCatbrow = [ + "/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit", + "/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit" + ].includes(kubrowPetName); + const traitsPool = isCatbrow ? catbrowDetails : kubrowDetails; + + traits = { + BaseColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + SecondaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + TertiaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + AccentColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + EyeColor: getRandomWeightedReward(traitsPool.EyeColors, kubrowWeights)!.type, + FurPattern: getRandomWeightedReward(traitsPool.FurPatterns, kubrowFurPatternsWeights)!.type, + Personality: kubrowPetName, + BodyType: getRandomWeightedReward(traitsPool.BodyTypes, kubrowWeights)!.type, + Head: isCatbrow ? getRandomWeightedReward(traitsPool.Heads, kubrowWeights)!.type : undefined, + Tail: isCatbrow ? getRandomWeightedReward(traitsPool.Tails, kubrowWeights)!.type : undefined + }; + } + + details = { + Name: "", + IsPuppy: false, + HasCollar: true, + PrintsRemaining: 2, + Status: Status.StatusStasis, + HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000), + IsMale: !!getRandomInt(0, 1), + Size: getRandomInt(70, 100) / 100, + DominantTraits: traits, + RecessiveTraits: traits + }; + } + + const kubrowPetIndex = + inventory.KubrowPets.push({ + ItemType: kubrowPetName, + Configs: configs, + XP: 0, + Details: details, + IsNew: true + }) - 1; + inventoryChanges.KubrowPets ??= []; + inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON()); + + return inventoryChanges; +}; + export const updateSlots = ( inventory: TInventoryDatabaseDocument, slotName: SlotNames, diff --git a/static/webui/index.html b/static/webui/index.html index 3830e378..5df7ba59 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -305,7 +305,7 @@ -
+
+
+
+
+ + + + + + + + +
+
+
+
@@ -700,6 +722,7 @@ + @@ -731,6 +754,10 @@ + + + + diff --git a/static/webui/script.js b/static/webui/script.js index ed71ed96..2e4477bb 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -163,7 +163,13 @@ const webUiModularWeapons = [ "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit" ]; let uniqueLevelCaps = {}; @@ -308,7 +314,11 @@ function fetchItemList() { "LWPT_ZANUKA_BODY", "LWPT_ZANUKA_HEAD", "LWPT_ZANUKA_LEG", - "LWPT_ZANUKA_TAIL" + "LWPT_ZANUKA_TAIL", + "LWPT_CATBROW_ANTIGEN", + "LWPT_CATBROW_MUTAGEN", + "LWPT_KUBROW_ANTIGEN", + "LWPT_KUBROW_MUTAGEN" ]; if (supportedModularParts.includes(item.partType)) { const option = document.createElement("option"); @@ -361,7 +371,13 @@ function updateInventory() { "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit" ]; // Populate inventory route @@ -384,7 +400,8 @@ function updateInventory() { "Hoverboards", "OperatorAmps", "MechSuits", - "MoaPets" + "MoaPets", + "KubrowPets" ].forEach(category => { document.getElementById(category + "-list").innerHTML = ""; data[category].forEach(item => { @@ -462,6 +479,23 @@ function updateInventory() { a.innerHTML = ``; td.appendChild(a); } + if (category == "KubrowPets") { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + maturePet(item.ItemId.$oid, !item.Details.IsPuppy); + }; + if (item.Details.IsPuppy) { + a.title = loc("code_mature"); + a.innerHTML = ``; + } else { + a.title = loc("code_unmature"); + a.innerHTML = ``; + } + + td.appendChild(a); + } if (category == "Suits") { const a = document.createElement("a"); a.href = "/webui/powersuit/" + item.ItemId.$oid; @@ -870,12 +904,12 @@ function doAcquireEquipment(category) { }); } -function doAcquireModularEquipment(category, ItemType) { +function doAcquireModularEquipment(category, WeaponType) { let requiredParts; - let ModularParts = []; + let Parts = []; switch (category) { case "HoverBoards": - ItemType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"; + WeaponType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"; requiredParts = ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"]; break; case "OperatorAmps": @@ -891,20 +925,33 @@ function doAcquireModularEquipment(category, ItemType) { requiredParts = ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"]; break; case "MoaPets": - if (ItemType == "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { + if (WeaponType == "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { requiredParts = ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"]; } else { requiredParts = ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"]; } break; + case "KubrowPets": + if ( + [ + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit" + ].includes(WeaponType) + ) { + requiredParts = ["CATBROW_ANTIGEN", "CATBROW_MUTAGEN"]; + } else { + requiredParts = ["KUBROW_ANTIGEN", "KUBROW_MUTAGEN"]; + } + break; } requiredParts.forEach(part => { const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part)); if (partName) { - ModularParts.push(partName); + Parts.push(partName); } }); - if (ModularParts.length != requiredParts.length) { + if (Parts.length != requiredParts.length) { let isFirstPart = true; requiredParts.forEach(part => { const partSelector = document.getElementById("acquire-type-" + category + "-" + part); @@ -920,20 +967,80 @@ function doAcquireModularEquipment(category, ItemType) { } }); } else { + const mapping = { + LongGuns: { + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelAPart": + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + "/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelEgg/InfModularBarrelEggPart": + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart": + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelCPart": + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelDPart": + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam", + "/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelBeam/InfModularBarrelBeamPart": + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" + }, + Pistols: { + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelAPart": + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + "/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelEgg/InfModularBarrelEggPart": + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart": + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelCPart": + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelDPart": + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam", + "/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelBeam/InfModularBarrelBeamPart": + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }, + MoaPets: { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + } + }; + + Parts.forEach(part => { + const categoryMap = mapping[category]; + if (categoryMap && categoryMap[part]) { + WeaponType = categoryMap[part]; + } + }); + if (category == "KubrowPets") Parts.unshift(WeaponType); revalidateAuthz(() => { const req = $.post({ - url: "/custom/addModularEquipment?" + window.authz, - contentType: "application/json", + url: "/api/modularWeaponCrafting.php?" + window.authz, + contentType: "application/octet-stream", data: JSON.stringify({ - ItemType, - ModularParts + WeaponType, + Parts, + isWebUi: true }) }); req.done(() => { const mainInput = document.getElementById("acquire-type-" + category); if (mainInput) { mainInput.value = ""; - document.getElementById("modular-" + category).style.display = "none"; + if (category === "MoaPets") { + const modularFieldsMoa = document.getElementById("modular-MoaPets-Moa"); + const modularFieldsZanuka = document.getElementById("modular-MoaPets-Zanuka"); + modularFieldsZanuka.style.display = "none"; + modularFieldsMoa.style.display = "none"; + } else if (category === "KubrowPets") { + const modularFieldsCatbrow = document.getElementById("modular-KubrowPets-Catbrow"); + const modularFieldsKubrow = document.getElementById("modular-KubrowPets-Kubrow"); + modularFieldsCatbrow.style.display = "none"; + modularFieldsKubrow.style.display = "none"; + } else { + const modularFields = document.getElementById("modular-" + category); + modularFields.style.display = "none"; + } } requiredParts.forEach(part => { document.getElementById("acquire-type-" + category + "-" + part).value = ""; @@ -1085,24 +1192,37 @@ function renameGear(category, oid, name) { } function disposeOfGear(category, oid) { - const data = { - SellCurrency: "SC_RegularCredits", - SellPrice: 0, - Items: {} - }; - data.Items[category] = [ - { - String: oid, - Count: 0 - } - ]; - revalidateAuthz(() => { - $.post({ - url: "/api/sell.php?" + window.authz, - contentType: "text/plain", - data: JSON.stringify(data) + if (category == "KubrowPets") { + revalidateAuthz(() => { + $.post({ + url: "/api/releasePet.php?" + window.authz, + contentType: "application/octet-stream", + data: JSON.stringify({ + Recipe: "webui", + petId: oid + }) + }); }); - }); + } else { + const data = { + SellCurrency: "SC_RegularCredits", + SellPrice: 0, + Items: {} + }; + data.Items[category] = [ + { + String: oid, + Count: 0 + } + ]; + revalidateAuthz(() => { + $.post({ + url: "/api/sell.php?" + window.authz, + contentType: "text/plain", + data: JSON.stringify(data) + }); + }); + } } function disposeOfItems(category, type, count) { @@ -1140,6 +1260,21 @@ function gildEquipment(category, oid) { }); } +function maturePet(oid, revert) { + revalidateAuthz(() => { + $.post({ + url: "/api/maturePet.php?" + window.authz, + contentType: "application/octet-stream", + data: JSON.stringify({ + petId: oid, + revert + }) + }).done(function () { + updateInventory(); + }); + }); +} + function doAcquireMiscItems() { const uniqueName = getKey(document.getElementById("miscitem-type")); if (!uniqueName) { @@ -1589,32 +1724,60 @@ function handleModularSelection(category) { } } { - const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols", "MoaPets"]; + const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols", "MoaPets", "KubrowPets"]; supportedModularInventoryCategory.forEach(inventoryCategory => { document.getElementById("acquire-type-" + inventoryCategory).addEventListener("input", function () { const modularFields = document.getElementById("modular-" + inventoryCategory); - const modularFieldsZanuka = - inventoryCategory === "MoaPets" - ? document.getElementById("modular-" + inventoryCategory + "-Zanuka") - : null; + const modularFieldsMoa = document.getElementById("modular-MoaPets-Moa"); + const modularFieldsZanuka = document.getElementById("modular-MoaPets-Zanuka"); + const modularFieldsCatbrow = document.getElementById("modular-KubrowPets-Catbrow"); + const modularFieldsKubrow = document.getElementById("modular-KubrowPets-Kubrow"); const key = getKey(this); if (webUiModularWeapons.includes(key)) { - if (key === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" && modularFieldsZanuka) { - modularFields.style.display = "none"; - modularFieldsZanuka.style.display = ""; - } else if (key === "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { - modularFields.style.display = ""; - if (modularFieldsZanuka) { + if (inventoryCategory === "MoaPets") { + if (key === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") { + modularFieldsMoa.style.display = "none"; + modularFieldsZanuka.style.display = ""; + } else if (key === "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { + modularFieldsMoa.style.display = ""; modularFieldsZanuka.style.display = "none"; } + } else if (inventoryCategory === "KubrowPets") { + if ( + [ + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit" + ].includes(key) + ) { + modularFieldsCatbrow.style.display = ""; + modularFieldsKubrow.style.display = "none"; + } else if ( + [ + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit", + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit" + ].includes(key) + ) { + modularFieldsCatbrow.style.display = "none"; + modularFieldsKubrow.style.display = ""; + } else { + modularFieldsCatbrow.style.display = "none"; + modularFieldsKubrow.style.display = "none"; + } } else { modularFields.style.display = ""; } } else { - modularFields.style.display = "none"; - if (modularFieldsZanuka) { + if (inventoryCategory === "MoaPets") { modularFieldsZanuka.style.display = "none"; + modularFieldsMoa.style.display = "none"; + } else if (inventoryCategory === "KubrowPets") { + modularFieldsCatbrow.style.display = "none"; + modularFieldsKubrow.style.display = "none"; + } else { + modularFields.style.display = "none"; } } }); diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index abcae96c..061da9c4 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -54,6 +54,8 @@ dict = { code_completed: `Abgeschlossen`, code_active: `Aktiv`, code_pigment: `Pigment`, + code_mature: `[UNTRANSLATED] Mature for combat`, + code_unmature: `[UNTRANSLATED] Regress genetic aging`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, @@ -80,6 +82,7 @@ dict = { inventory_operatorAmps: `Verstärker`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, + inventory_kubrowPets: `[UNTRANSLATED] Beasts`, inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 3b6fb449..cd27b42d 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -53,6 +53,8 @@ dict = { code_completed: `Completed`, code_active: `Active`, code_pigment: `Pigment`, + code_mature: `Mature for combat`, + code_unmature: `Regress genetic aging`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, @@ -79,6 +81,7 @@ dict = { inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, + inventory_kubrowPets: `Beasts`, inventory_bulkAddSuits: `Add Missing Warframes`, inventory_bulkAddWeapons: `Add Missing Weapons`, inventory_bulkAddSpaceSuits: `Add Missing Archwings`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index f51f506a..00a80ecc 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -54,6 +54,8 @@ dict = { code_completed: `Completada`, code_active: `Activa`, code_pigment: `Pigmento`, + code_mature: `[UNTRANSLATED] Mature for combat`, + code_unmature: `[UNTRANSLATED] Regress genetic aging`, login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`, login_emailLabel: `Dirección de correo electrónico`, login_passwordLabel: `Contraseña`, @@ -80,6 +82,7 @@ dict = { inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, + inventory_kubrowPets: `[UNTRANSLATED] Beasts`, inventory_bulkAddSuits: `Agregar Warframes faltantes`, inventory_bulkAddWeapons: `Agregar armas faltantes`, inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index b1938b53..bf76ac9e 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -54,6 +54,8 @@ dict = { code_completed: `[UNTRANSLATED] Completed`, code_active: `[UNTRANSLATED] Active`, code_pigment: `Pigment`, + code_mature: `[UNTRANSLATED] Mature for combat`, + code_unmature: `[UNTRANSLATED] Regress genetic aging`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, @@ -80,6 +82,7 @@ dict = { inventory_operatorAmps: `Amplificateurs`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, + inventory_kubrowPets: `[UNTRANSLATED] Beasts`, inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, inventory_bulkAddWeapons: `Ajouter les armes manquantes`, inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 4a5dceeb..b299c14c 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -54,6 +54,8 @@ dict = { code_completed: `Завершено`, code_active: `Активный`, code_pigment: `Пигмент`, + code_mature: `Подготовить к сражениям`, + code_unmature: `Регрессия генетического старения`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, @@ -80,6 +82,7 @@ dict = { inventory_operatorAmps: `Усилители`, inventory_hoverboards: `К-Драйвы`, inventory_moaPets: `МОА`, + inventory_kubrowPets: `Звери`, inventory_bulkAddSuits: `Добавить отсутствующие варфреймы`, inventory_bulkAddWeapons: `Добавить отсутствующее оружие`, inventory_bulkAddSpaceSuits: `Добавить отсутствующие арчвинги`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 5af41bea..f97ec5d2 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -54,6 +54,8 @@ dict = { code_completed: `[UNTRANSLATED] Completed`, code_active: `[UNTRANSLATED] Active`, code_pigment: `颜料`, + code_mature: `[UNTRANSLATED] Mature for combat`, + code_unmature: `[UNTRANSLATED] Regress genetic aging`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, @@ -80,6 +82,7 @@ dict = { inventory_operatorAmps: `增幅器`, inventory_hoverboards: `K式悬浮板`, inventory_moaPets: `恐鸟`, + inventory_kubrowPets: `[UNTRANSLATED] Beasts`, inventory_bulkAddSuits: `添加缺失战甲`, inventory_bulkAddWeapons: `添加缺失武器`, inventory_bulkAddSpaceSuits: `添加缺失Archwing`, From 9912a623b10683632fe8656b816da853215037d8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:42:56 -0700 Subject: [PATCH 552/776] fix: complete all quests not working (#1755) Fixes #1742 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1755 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 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 04650a06..b951c754 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -35,10 +35,8 @@ export const manageQuestsController: RequestHandler = async (req, res) => { switch (operation) { case "completeAll": { - if (allQuestKeys.includes(questItemType)) { - for (const questKey of inventory.QuestKeys) { - await completeQuest(inventory, questKey.ItemType); - } + for (const questKey of inventory.QuestKeys) { + await completeQuest(inventory, questKey.ItemType); } break; } From 98aebba677f4f15ce2529972e38a0f697b54c0dd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:43:10 -0700 Subject: [PATCH 553/776] fix: EOM endo rewards showing as doubled in the client (#1756) Closes #1754 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1756 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/missionInventoryUpdateController.ts | 2 +- src/types/purchaseTypes.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 3eb0762c..ead3b56e 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -84,7 +84,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) MissionRewards, ...credits, ...inventoryUpdates, - FusionPoints: inventoryChanges?.FusionPoints, + //FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed. SyndicateXPItemReward, AffiliationMods }); diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index eac812a2..3600e6b7 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -39,6 +39,7 @@ export type IInventoryChanges = { RegularCredits?: number; PremiumCredits?: number; PremiumCreditsFree?: number; + FusionPoints?: number; PrimeTokens?: number; InfestedFoundry?: IInfestedFoundryClient; Drones?: IDroneClient[]; From bdf0ac722b5ec05c930ae43c1888a20d6ebd430c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:43:26 -0700 Subject: [PATCH 554/776] feat: give lotus eaters quest at completion of whispers in the walls (#1760) Closes #1758 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1760 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/fixed_responses/questCompletionRewards.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/fixed_responses/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json index 71fb423e..1e584e72 100644 --- a/static/fixed_responses/questCompletionRewards.json +++ b/static/fixed_responses/questCompletionRewards.json @@ -11,5 +11,11 @@ "ItemType": "/Lotus/Types/Keys/RailJackBuildQuest/RailjackBuildQuestEmailItem", "ItemCount": 1 } + ], + "/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain": [ + { + "ItemType": "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain", + "ItemCount": 1 + } ] } From 72b28f1d75e0e51ca0c26ec2c9fa64bda142ef85 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:44:01 -0700 Subject: [PATCH 555/776] feat: send hex quest email when The Lotus Eaters and The Duviri Paradox are complete (#1761) Close #1759 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1761 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/questService.ts | 48 +++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/services/questService.ts b/src/services/questService.ts index d5ac28dc..053f6eb4 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -191,6 +191,25 @@ const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => return items; }; +// Checks that `questKey` is in `requirements`, and if so, that all other quests in `requirements` are also already completed. +const doesQuestCompletionFinishSet = ( + inventory: TInventoryDatabaseDocument, + questKey: string, + requirements: string[] +): boolean => { + let holds = false; + for (const requirement of requirements) { + if (questKey == requirement) { + holds = true; + } else { + if (!inventory.QuestKeys.find(x => x.ItemType == requirement)?.Completed) { + return false; + } + } + } + return holds; +}; + const handleQuestCompletion = async ( inventory: TInventoryDatabaseDocument, questKey: string, @@ -218,12 +237,10 @@ const handleQuestCompletion = async ( // Whispers in the Walls is unlocked once The New + Heart of Deimos are completed. if ( - (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && - inventory.QuestKeys.find( - x => x.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" - )?.Completed) || - (questKey == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" && - inventory.QuestKeys.find(x => x.ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain")?.Completed) + doesQuestCompletionFinishSet(inventory, questKey, [ + "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain", + "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" + ]) ) { await createMessage(inventory.accountOwnerId, [ { @@ -237,6 +254,25 @@ const handleQuestCompletion = async ( ]); } + // The Hex (Quest) is unlocked once The Lotus Eaters + The Duviri Paradox are completed. + if ( + doesQuestCompletionFinishSet(inventory, questKey, [ + "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain", + "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain" + ]) + ) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/NewWar/P3M1ChooseMara", + msg: "/Lotus/Language/1999Quest/1999QuestInboxBody", + att: ["/Lotus/Types/Keys/1999Quest/1999QuestKeyChain"], + sub: "/Lotus/Language/1999Quest/1999QuestInboxSubject", + icon: "/Lotus/Interface/Icons/Npcs/Operator.png", + highPriority: true + } + ]); + } + const questCompletionItems = getQuestCompletionItems(questKey); logger.debug(`quest completion items`, questCompletionItems); if (questCompletionItems) { From ec6729db4d5756e52448a34fb05e43e7e44b5431 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:44:12 -0700 Subject: [PATCH 556/776] feat: setHubNpcCustomizations (#1762) Closes #1757 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1762 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/setHubNpcCustomizationsController.ts | 21 +++++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 16 ++++++++++++-- src/routes/api.ts | 2 ++ src/types/inventoryTypes/inventoryTypes.ts | 7 +++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/setHubNpcCustomizationsController.ts diff --git a/src/controllers/api/setHubNpcCustomizationsController.ts b/src/controllers/api/setHubNpcCustomizationsController.ts new file mode 100644 index 00000000..6e199933 --- /dev/null +++ b/src/controllers/api/setHubNpcCustomizationsController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IHubNpcCustomization } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const setHubNpcCustomizationsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "HubNpcCustomizations"); + const upload = getJSONfromString(String(req.body)); + inventory.HubNpcCustomizations ??= []; + const cust = inventory.HubNpcCustomizations.find(x => x.Tag == upload.Tag); + if (cust) { + cust.Colors = upload.Colors; + cust.Pattern = upload.Pattern; + } else { + inventory.HubNpcCustomizations.push(upload); + } + await inventory.save(); + res.end(); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 029bf6c0..775de04a 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -95,7 +95,8 @@ import { ISortieRewardAttenuation, IInvasionProgressDatabase, IInvasionProgressClient, - IAccolades + IAccolades, + IHubNpcCustomization } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1327,6 +1328,15 @@ const lockedWeaponGroupSchema = new Schema( { _id: false } ); +const hubNpcCustomizationSchema = new Schema( + { + Colors: colorSchema, + Pattern: String, + Tag: String + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1680,7 +1690,9 @@ const inventorySchema = new Schema( // G3 + Zanuka BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined }, - LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined } + LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined }, + + HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); diff --git a/src/routes/api.ts b/src/routes/api.ts index e6416f7a..c3dc2e50 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -117,6 +117,7 @@ import { setDojoComponentMessageController } from "@/src/controllers/api/setDojo import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; +import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; @@ -285,6 +286,7 @@ apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); apiRouter.post("/setGuildMotd.php", setGuildMotdController); +apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index b4dbc57c..219f43a5 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -368,6 +368,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EchoesHexConquestActiveStickers?: string[]; BrandedSuits?: IOid[]; LockedWeaponGroup?: ILockedWeaponGroupClient; + HubNpcCustomizations?: IHubNpcCustomization[]; } export interface IAffiliation { @@ -1228,3 +1229,9 @@ export interface ILockedWeaponGroupDatabase { } export type TPartialStartingGear = Pick; + +export interface IHubNpcCustomization { + Colors?: IColor; + Pattern: string; + Tag: string; +} From e3a34399e50aaf3b484e778ed822ba5746c9f6d5 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Mon, 21 Apr 2025 11:55:58 -0700 Subject: [PATCH 557/776] chore(webui): update to Spanish translation (#1772) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1772 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 00a80ecc..96916759 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -54,8 +54,8 @@ dict = { code_completed: `Completada`, code_active: `Activa`, code_pigment: `Pigmento`, - code_mature: `[UNTRANSLATED] Mature for combat`, - code_unmature: `[UNTRANSLATED] Regress genetic aging`, + code_mature: `Listo para el combate`, + code_unmature: `Regresar el envejecimiento genético`, login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`, login_emailLabel: `Dirección de correo electrónico`, login_passwordLabel: `Contraseña`, @@ -82,7 +82,7 @@ dict = { inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, - inventory_kubrowPets: `[UNTRANSLATED] Beasts`, + inventory_kubrowPets: `Bestias`, inventory_bulkAddSuits: `Agregar Warframes faltantes`, inventory_bulkAddWeapons: `Agregar armas faltantes`, inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`, From a49edefbd1db19b7b4f2728bc7a21cce90f43fe5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 15:21:40 -0700 Subject: [PATCH 558/776] fix(webui): don't halve required R30 XP for MoaPets & KubrowPets (#1771) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1771 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index 2e4477bb..a2b8619c 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -436,7 +436,9 @@ function updateInventory() { category != "SpaceSuits" && category != "Sentinels" && category != "Hoverboards" && - category != "MechSuits" + category != "MechSuits" && + category != "MoaPets" && + category != "KubrowPets" ) { maxXP /= 2; } From 731be0d5e339f90b134a86d81e1e764d9961f319 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:06:39 -0700 Subject: [PATCH 559/776] fix: exclude MT_ARENA from sortie node options (#1769) Fixes #1764 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1769 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index d8cd0c3c..2ca13620 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -232,6 +232,7 @@ const pushSortieIfRelevant = (out: ISortie[], day: number): void => { value.missionIndex != 0 && // Exclude MT_ASSASSINATION value.missionIndex != 5 && // Exclude MT_CAPTURE value.missionIndex != 21 && // Exclude MT_PURIFY + value.missionIndex != 22 && // Exclude MT_ARENA value.missionIndex != 23 && // Exclude MT_JUNCTION value.missionIndex <= 28 ) { From c94bc3ef90a88dae9fc6301ae603c5722a3233f4 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Tue, 22 Apr 2025 00:00:52 -0700 Subject: [PATCH 560/776] chore(webui): update Russian translation (#1783) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1783 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index b299c14c..97956a1b 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -108,7 +108,7 @@ dict = { currency_owned: `У тебя |COUNT|.`, powersuit_archonShardsLabel: `Ячейки осколков архонта`, powersuit_archonShardsDescription: `Вы можете использовать эти неограниченные ячейки для установки множества улучшений.`, - powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, + powersuit_archonShardsDescription2: `Обратите внимание: каждый фрагмент архонта применяется с задержкой при загрузке.`, mods_addRiven: `Добавить Мод Разлома`, mods_fingerprint: `Отпечаток`, mods_fingerprintHelp: `Нужна помощь с отпечатком?`, @@ -136,15 +136,15 @@ dict = { cheats_unlockDoubleCapacityPotatoesEverywhere: `Катализаторы везде`, cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, - cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, - cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, + cheats_noDailyStandingLimits: `Без ежедневных лимитов репутации`, + cheats_noDailyFocusLimit: `Без ежедневных лимитов фокуса`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, - cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, + cheats_noDeathMarks: `Без меток сметри`, + cheats_noKimCooldowns: `Чаты KIM без кулдауна`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, + cheats_noResourceExtractorDronesDamage: `Без урона по дронам-сборщикам`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, From e3fca682d6f2c7cf139e47c32feddedc7f713acb Mon Sep 17 00:00:00 2001 From: Vitruvio Date: Tue, 22 Apr 2025 09:44:07 -0700 Subject: [PATCH 561/776] Update static/webui/translations/fr.js (#1784) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1784 Co-authored-by: Vitruvio Co-committed-by: Vitruvio --- static/webui/translations/fr.js | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index bf76ac9e..11b310d1 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -25,10 +25,10 @@ dict = { code_renamePrompt: `Nouveau nom :`, code_remove: `Retirer`, code_addItemsConfirm: `Ajouter |COUNT| items à l'inventaire ?`, - code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, - code_noEquipmentToRankUp: `No equipment to rank up.`, + code_succRankUp: `Montée de niveau effectuée.`, + code_noEquipmentToRankUp: `Aucun équipement à monter de niveau.`, code_succAdded: `Ajouté.`, - code_succRemoved: `[UNTRANSLATED] Successfully removed.`, + code_succRemoved: `Retiré.`, code_buffsNumber: `Nombre de buffs`, code_cursesNumber: `Nombre de débuffs`, code_rerollsNumber: `Nombre de rerolls`, @@ -45,17 +45,17 @@ dict = { code_zanukaA: `Molosse Dorma`, code_zanukaB: `Molosse Bhaira`, code_zanukaC: `Molosse Hec`, - code_stage: `[UNTRANSLATED] Stage`, - code_complete: `[UNTRANSLATED] Complete`, - code_nextStage: `[UNTRANSLATED] Next stage`, - code_prevStage: `[UNTRANSLATED] Previous stage`, - code_reset: `[UNTRANSLATED] Reset`, - code_setInactive: `[UNTRANSLATED] Make the quest inactive`, - code_completed: `[UNTRANSLATED] Completed`, - code_active: `[UNTRANSLATED] Active`, + code_stage: `Étape`, + code_complete: `Compléter`, + code_nextStage: `Étape suivante`, + code_prevStage: `Étape précédente`, + code_reset: `Réinitialiser`, + code_setInactive: `Rendre la quête inactive`, + code_completed: `Complétée`, + code_active: `Active`, code_pigment: `Pigment`, - code_mature: `[UNTRANSLATED] Mature for combat`, - code_unmature: `[UNTRANSLATED] Regress genetic aging`, + code_mature: `Maturer pour le combat`, + code_unmature: `Régrésser l'âge génétique`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, @@ -82,7 +82,7 @@ dict = { inventory_operatorAmps: `Amplificateurs`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, - inventory_kubrowPets: `[UNTRANSLATED] Beasts`, + inventory_kubrowPets: `Bêtes`, inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, inventory_bulkAddWeapons: `Ajouter les armes manquantes`, inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, @@ -107,8 +107,8 @@ dict = { currency_PrimeTokens: `Aya Raffiné`, currency_owned: `|COUNT| possédés.`, powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`, - powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations.`, - powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, + powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations`, + powersuit_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`, mods_addRiven: `Ajouter un riven`, mods_fingerprint: `Empreinte`, mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, @@ -131,32 +131,32 @@ dict = { cheats_unlockAllFlavourItems: `Débloquer tous les Flavor Items`, cheats_unlockAllSkins: `Débloquer tous les skins`, cheats_unlockAllCapturaScenes: `Débloquer toutes les scènes captura`, - cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`, + cheats_unlockAllDecoRecipes: `Débloquer toutes les recherches dojo`, cheats_universalPolarityEverywhere: `Polarités universelles partout`, cheats_unlockDoubleCapacityPotatoesEverywhere: `Réacteurs et Catalyseurs partout`, cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, - cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, - cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, - cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, - cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, - cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, - cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, - cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, - cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, - cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, - cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, + cheats_noDailyStandingLimits: `Aucune limite de réputation journalière`, + cheats_noDailyFocusLimit: `Aucune limite journalière de focus`, + cheats_noArgonCrystalDecay: `Aucune désintégration des Cristaux d'Argon`, + cheats_noMasteryRankUpCooldown: `Aucune attente pour la montée de rang de maîtrise`, + cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`, + cheats_noDeathMarks: `Aucune marque d'assassin`, + cheats_noKimCooldowns: `Aucun cooldown sur le KIM`, + cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`, + cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`, + cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`, + cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`, + cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`, cheats_noDojoResearchCosts: `Aucun coût de recherche (Dojo)`, cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, - cheats_fastClanAscension: `[UNTRANSLATED] Fast Clan Ascension`, - cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, + cheats_fastClanAscension: `Ascension de clan rapide`, + cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`, cheats_saveSettings: `Sauvegarder les paramètres`, cheats_account: `Compte`, cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`, cheats_helminthUnlockAll: `Helminth niveau max`, - cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, + cheats_intrinsicsUnlockAll: `Inhérences niveau max`, cheats_changeSupportedSyndicate: `Allégeance`, cheats_changeButton: `Changer`, cheats_none: `Aucun`, From 03590c73606c91ee4bb5986e7d48684141831a91 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Tue, 22 Apr 2025 09:44:47 -0700 Subject: [PATCH 562/776] chore(webui): update German translation (#1786) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1786 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 061da9c4..40bb5667 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -54,8 +54,8 @@ dict = { code_completed: `Abgeschlossen`, code_active: `Aktiv`, code_pigment: `Pigment`, - code_mature: `[UNTRANSLATED] Mature for combat`, - code_unmature: `[UNTRANSLATED] Regress genetic aging`, + code_mature: `Für den Kampf auswachsen lassen`, + code_unmature: `Genetisches Altern zurücksetzen`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, @@ -82,7 +82,7 @@ dict = { inventory_operatorAmps: `Verstärker`, inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, - inventory_kubrowPets: `[UNTRANSLATED] Beasts`, + inventory_kubrowPets: `Bestien`, inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, From ad2f143f1507467b8d56392957c7f223ee2c2644 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:59:30 -0700 Subject: [PATCH 563/776] feat: cleanup some problems in inventories at daily reset (#1767) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1767 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 | 9 ++++++++- src/services/inventoryService.ts | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index bede097f..ab739c4c 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -14,7 +14,12 @@ import { ExportVirtuals } from "warframe-public-export-plus"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; -import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; +import { + addMiscItems, + allDailyAffiliationKeys, + cleanupInventory, + createLibraryDailyTask +} from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; import { catBreadHash } from "@/src/helpers/stringHelpers"; @@ -79,6 +84,8 @@ export const inventoryController: RequestHandler = async (request, response) => } } + cleanupInventory(inventory); + inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); await inventory.save(); } diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index fd6a597b..c9e81c78 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1769,3 +1769,23 @@ export const setupKahlSyndicate = (inventory: TInventoryDatabaseDocument): void Tag: "KahlSyndicate" }); }; + +export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => { + let index = inventory.MiscItems.findIndex(x => x.ItemType == ""); + if (index != -1) { + inventory.MiscItems.splice(index, 1); + } + + index = inventory.Affiliations.findIndex(x => x.Tag == "KahlSyndicate"); + if (index != -1 && !inventory.Affiliations[index].WeeklyMissions) { + logger.debug(`KahlSyndicate seems broken, removing it and setting up again`); + inventory.Affiliations.splice(index, 1); + setupKahlSyndicate(inventory); + } + + const LibrarySyndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate"); + if (LibrarySyndicate && LibrarySyndicate.FreeFavorsEarned) { + logger.debug(`removing FreeFavorsEarned from LibrarySyndicate`); + LibrarySyndicate.FreeFavorsEarned = undefined; + } +}; From 6d93ae9f2dcb8d109d14d8804deed690ed205280 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:59:41 -0700 Subject: [PATCH 564/776] fix: be less strict with required avatar type for personal synthesis (#1768) Fixes #1766 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1768 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 4dcb0fe4..46d039c1 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -55,6 +55,7 @@ import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; import { config } from "./configService"; +import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C @@ -331,35 +332,36 @@ export const addMissionInventoryUpdates = async ( case "LibraryScans": value.forEach(scan => { let synthesisIgnored = true; - if ( - inventory.LibraryPersonalTarget && - libraryPersonalTargetToAvatar[inventory.LibraryPersonalTarget] == scan.EnemyType - ) { - let progress = inventory.LibraryPersonalProgress.find( - x => x.TargetType == inventory.LibraryPersonalTarget - ); - if (!progress) { - progress = - inventory.LibraryPersonalProgress[ - inventory.LibraryPersonalProgress.push({ - TargetType: inventory.LibraryPersonalTarget, - Scans: 0, - Completed: false - }) - 1 - ]; + if (inventory.LibraryPersonalTarget) { + const taskAvatar = libraryPersonalTargetToAvatar[inventory.LibraryPersonalTarget]; + const taskAvatars = libraryDailyTasks.find(x => x.indexOf(taskAvatar) != -1)!; + if (taskAvatars.indexOf(scan.EnemyType) != -1) { + let progress = inventory.LibraryPersonalProgress.find( + x => x.TargetType == inventory.LibraryPersonalTarget + ); + if (!progress) { + progress = + inventory.LibraryPersonalProgress[ + inventory.LibraryPersonalProgress.push({ + TargetType: inventory.LibraryPersonalTarget, + Scans: 0, + Completed: false + }) - 1 + ]; + } + progress.Scans += scan.Count; + if ( + progress.Scans >= + (inventory.LibraryPersonalTarget == + "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget" + ? 3 + : 10) + ) { + progress.Completed = true; + } + logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`); + synthesisIgnored = false; } - progress.Scans += scan.Count; - if ( - progress.Scans >= - (inventory.LibraryPersonalTarget == - "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget" - ? 3 - : 10) - ) { - progress.Completed = true; - } - logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`); - synthesisIgnored = false; } if ( inventory.LibraryActiveDailyTaskInfo && From 3b20a109f65fa7f9763f1abcd463ca0ff094457f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:59:54 -0700 Subject: [PATCH 565/776] fix: handle saveDialogue without YearIteration having been supplied (#1774) This is needed for The Hex rank up dialogues, which are independent of the year iterations. Fixes #1773 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1774 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 11 +++-------- src/models/inventoryModels/inventoryModel.ts | 2 +- src/types/inventoryTypes/inventoryTypes.ts | 4 ++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 7d7d6380..49cc605c 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -12,22 +12,17 @@ export const saveDialogueController: RequestHandler = async (req, res) => { const request = JSON.parse(String(req.body)) as SaveDialogueRequest; if ("YearIteration" in request) { const inventory = await getInventory(accountId, "DialogueHistory"); - if (inventory.DialogueHistory) { - inventory.DialogueHistory.YearIteration = request.YearIteration; - } else { - inventory.DialogueHistory = { YearIteration: request.YearIteration }; - } + inventory.DialogueHistory ??= {}; + inventory.DialogueHistory.YearIteration = request.YearIteration; await inventory.save(); res.end(); } else { const inventory = await getInventory(accountId); - if (!inventory.DialogueHistory) { - throw new Error("bad inventory state"); - } const inventoryChanges: IInventoryChanges = {}; const tomorrowAt0Utc = config.noKimCooldowns ? Date.now() : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + inventory.DialogueHistory ??= {}; inventory.DialogueHistory.Dialogues ??= []; const dialogue = getDialogue(inventory, request.DialogueName); dialogue.Rank = request.Rank; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 775de04a..6b73186f 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -909,7 +909,7 @@ dialogueSchema.set("toJSON", { const dialogueHistorySchema = new Schema( { - YearIteration: { type: Number, required: true }, + YearIteration: Number, Resets: Number, Dialogues: { type: [dialogueSchema], required: false } }, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 219f43a5..8ddfbf42 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1130,13 +1130,13 @@ export interface IEndlessXpProgress { } export interface IDialogueHistoryClient { - YearIteration: number; + YearIteration?: number; Resets?: number; // added in 38.5.0 Dialogues?: IDialogueClient[]; } export interface IDialogueHistoryDatabase { - YearIteration: number; + YearIteration?: number; Resets?: number; Dialogues?: IDialogueDatabase[]; } From c4b8a71c5affbb99de264840d8d7b9a3d9c6cd74 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:00:04 -0700 Subject: [PATCH 566/776] chore(webui): provide "max rank" option when only exalted needs it (#1776) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1776 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index a2b8619c..d570d1b9 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -443,12 +443,28 @@ function updateInventory() { maxXP /= 2; } - if (item.XP < maxXP) { + let anyExaltedMissingXP = false; + if (item.XP >= maxXP && "exalted" in itemMap[item.ItemType]) { + for (const exaltedType of itemMap[item.ItemType].exalted) { + const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType); + if (exaltedItem) { + const exaltedCap = itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; + if (exaltedItem.XP < exaltedCap) { + anyExaltedMissingXP = true; + break; + } + } + } + } + + if (item.XP < maxXP || anyExaltedMissingXP) { const a = document.createElement("a"); a.href = "#"; a.onclick = function (event) { event.preventDefault(); - addGearExp(category, item.ItemId.$oid, maxXP - item.XP); + if (item.XP < maxXP) { + addGearExp(category, item.ItemId.$oid, maxXP - item.XP); + } if ("exalted" in itemMap[item.ItemType]) { for (const exaltedType of itemMap[item.ItemType].exalted) { const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType); From 409f41d3bf346d1807fb32f50cf90f5da33b7173 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:00:10 -0700 Subject: [PATCH 567/776] feat(webui): remove unranked mods (#1778) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1778 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/app.ts | 2 +- .../custom/getItemListsController.ts | 4 +++ static/webui/index.html | 3 ++- static/webui/script.js | 27 ++++++++++++++++++- 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/zh.js | 1 + 10 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/app.ts b/src/app.ts index c8b19583..291372f0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,7 +26,7 @@ app.use((req, _res, next) => { app.use(bodyParser.raw()); app.use(express.json({ limit: "4mb" })); -app.use(bodyParser.text()); +app.use(bodyParser.text({ limit: "4mb" })); app.use(requestLogger); app.use("/api", apiRouter); diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 214c51e7..1c7d95f4 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -29,6 +29,7 @@ interface ListedItem { badReason?: "starter" | "frivolous" | "notraw"; partType?: string; chainLength?: number; + parazon?: boolean; } const relicQualitySuffixes: Record = { @@ -196,6 +197,9 @@ const getItemListsController: RequestHandler = (req, response) => { } else if (upgrade.upgradeEntries) { mod.badReason = "notraw"; } + if (upgrade.type == "PARAZON") { + mod.parazon = true; + } res.mods.push(mod); } for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) { diff --git a/static/webui/index.html b/static/webui/index.html index 5df7ba59..eb131de2 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -490,8 +490,9 @@
-
+
+
diff --git a/static/webui/script.js b/static/webui/script.js index d570d1b9..562591fb 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -804,7 +804,7 @@ function updateInventory() { { const td = document.createElement("td"); td.classList = "text-end text-nowrap"; - { + if (maxRank != 0) { const a = document.createElement("a"); a.href = "#"; a.onclick = function (event) { @@ -1612,6 +1612,31 @@ function doAddAllMods() { }); } +function doRemoveUnrankedMods() { + revalidateAuthz(() => { + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + req.done(inventory => { + window.itemListPromise.then(itemMap => { + $.post({ + url: "/api/sell.php?" + window.authz, + contentType: "text/plain", + data: JSON.stringify({ + SellCurrency: "SC_RegularCredits", + SellPrice: 0, + Items: { + Upgrades: inventory.RawUpgrades.filter( + x => !itemMap[x.ItemType]?.parazon && x.ItemCount > 0 + ).map(x => ({ String: x.ItemType, Count: x.ItemCount })) + } + }) + }).done(function () { + updateInventory(); + }); + }); + }); + }); +} + // Powersuit Route single.getRoute("#powersuit-route").on("beforeload", function () { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 40bb5667..73ffcc83 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -115,6 +115,7 @@ dict = { mods_rivens: `Rivens`, mods_mods: `Mods`, mods_bulkAddMods: `Fehlende Mods hinzufügen`, + mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge |DISPLAYNAME| zu administratorNames in der config.json hinzu.`, cheats_server: `Server`, cheats_skipTutorial: `Tutorial überspringen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index cd27b42d..dc895457 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -114,6 +114,7 @@ dict = { mods_rivens: `Rivens`, mods_mods: `Mods`, mods_bulkAddMods: `Add Missing Mods`, + mods_removeUnranked: `Remove Unranked Mods`, cheats_administratorRequirement: `You must be an administrator to use this feature. To become an administrator, add |DISPLAYNAME| to administratorNames in the config.json.`, cheats_server: `Server`, cheats_skipTutorial: `Skip Tutorial`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 96916759..24eed094 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -115,6 +115,7 @@ dict = { mods_rivens: `Agrietados`, mods_mods: `Mods`, mods_bulkAddMods: `Agregar mods faltantes`, + mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega |DISPLAYNAME| a administratorNames en el archivo config.json.`, cheats_server: `Servidor`, cheats_skipTutorial: `Omitir tutorial`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 11b310d1..e174ec7d 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -115,6 +115,7 @@ dict = { mods_rivens: `Rivens`, mods_mods: `Mods`, mods_bulkAddMods: `Ajouter les mods manquants`, + mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez |DISPLAYNAME| à la ligne administratorNames dans le fichier config.json.`, cheats_server: `Serveur`, cheats_skipTutorial: `Passer le tutoriel`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 97956a1b..e3862a26 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -115,6 +115,7 @@ dict = { mods_rivens: `Моды Разлома`, mods_mods: `Моды`, mods_bulkAddMods: `Добавить отсутствующие моды`, + mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, cheats_administratorRequirement: `Вы должны быть администратором для использования этой функции. Чтобы стать администратором, добавьте \"|DISPLAYNAME|\" в administratorNames в config.json.`, cheats_server: `Сервер`, cheats_skipTutorial: `Пропустить обучение`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index f97ec5d2..ae832c7d 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -115,6 +115,7 @@ dict = { mods_rivens: `裂罅MOD`, mods_mods: `Mods`, mods_bulkAddMods: `添加缺失MOD`, + mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 |DISPLAYNAME| 添加到 config.json 的 administratorNames 中。`, cheats_server: `服务器`, cheats_skipTutorial: `跳过教程`, From 3aa853f953438f3265d6f471955395f35ccc9283 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:00:26 -0700 Subject: [PATCH 568/776] feat(webui): register (#1779) Closes #740 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1779 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/index.html | 1 + static/webui/script.js | 14 ++++++++++++-- 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/zh.js | 1 + 8 files changed, 19 insertions(+), 2 deletions(-) diff --git a/static/webui/index.html b/static/webui/index.html index eb131de2..65a9d832 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -85,6 +85,7 @@
+
diff --git a/static/webui/script.js b/static/webui/script.js index 562591fb..7868e189 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1,8 +1,15 @@ +let loginOrRegisterPending = false; +window.registerSubmit = false; + function doLogin() { + if (loginOrRegisterPending) { + return; + } + loginOrRegisterPending = true; localStorage.setItem("email", $("#email").val()); localStorage.setItem("password", $("#password").val()); - $("#email, #password").val(""); loginFromLocalStorage(); + registerSubmit = false; } function loginFromLocalStorage() { @@ -37,12 +44,15 @@ function doLoginRequest(succ_cb, fail_cb) { s: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==", // signature of some kind lang: "en", date: 1501230947855458660, // ??? - ClientType: "webui", + ClientType: registerSubmit ? "" : "webui", PS: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==" // anti-cheat data }) }); req.done(succ_cb); req.fail(fail_cb); + req.always(() => { + loginOrRegisterPending = false; + }); } function revalidateAuthz(succ_cb) { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 73ffcc83..b8725e00 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -60,6 +60,7 @@ dict = { login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, login_loginButton: `Anmelden`, + login_registerButton: `[UNTRANSLATED] Register`, navbar_logout: `Abmelden`, navbar_renameAccount: `Account umbenennen`, navbar_deleteAccount: `Account löschen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index dc895457..23b8133a 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -59,6 +59,7 @@ dict = { login_emailLabel: `Email address`, login_passwordLabel: `Password`, login_loginButton: `Login`, + login_registerButton: `Register`, navbar_logout: `Logout`, navbar_renameAccount: `Rename Account`, navbar_deleteAccount: `Delete Account`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 24eed094..222534cd 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -60,6 +60,7 @@ dict = { login_emailLabel: `Dirección de correo electrónico`, login_passwordLabel: `Contraseña`, login_loginButton: `Iniciar sesión`, + login_registerButton: `[UNTRANSLATED] Register`, navbar_logout: `Cerrar sesión`, navbar_renameAccount: `Renombrar cuenta`, navbar_deleteAccount: `Eliminar cuenta`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index e174ec7d..682086aa 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -60,6 +60,7 @@ dict = { login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, login_loginButton: `Connexion`, + login_registerButton: `[UNTRANSLATED] Register`, navbar_logout: `Déconnexion`, navbar_renameAccount: `Renommer le compte`, navbar_deleteAccount: `Supprimer le compte`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index e3862a26..959678f3 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -60,6 +60,7 @@ dict = { login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, login_loginButton: `Войти`, + login_registerButton: `[UNTRANSLATED] Register`, navbar_logout: `Выйти`, navbar_renameAccount: `Переименовать аккаунт`, navbar_deleteAccount: `Удалить аккаунт`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index ae832c7d..3f0e01f5 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -60,6 +60,7 @@ dict = { login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, login_loginButton: `登录`, + login_registerButton: `[UNTRANSLATED] Register`, navbar_logout: `退出登录`, navbar_renameAccount: `重命名账户`, navbar_deleteAccount: `删除账户`, From 23dafb53d1a1619f7dc8fe316984aa663e46ca2c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:00:38 -0700 Subject: [PATCH 569/776] fix: skipTutorial sets ReceivedStartingGear before giving the gear (#1780) This was raising a warning when creating a new account. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1780 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index c9e81c78..9e89dcfe 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -87,9 +87,7 @@ export const createInventory = async ( const inventory = new Inventory({ accountOwnerId: accountOwnerId, LoadOutPresets: defaultItemReferences.loadOutPresetId, - Ships: [defaultItemReferences.ship], - PlayedParkourTutorial: config.skipTutorial, - ReceivedStartingGear: config.skipTutorial + Ships: [defaultItemReferences.ship] }); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); @@ -102,6 +100,7 @@ export const createInventory = async ( await addItem(inventory, "/Lotus/Types/Friendly/PlayerControllable/Weapons/DuviriDualSwords"); if (config.skipTutorial) { + inventory.PlayedParkourTutorial = true; await addStartingGear(inventory); await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); From 32bb6d4ccbb0bcb8890679528612b5194984b113 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:00:49 -0700 Subject: [PATCH 570/776] feat: syndicate mission rotation (#1781) Closes #1530 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1781 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 67 +++++++++++++++---- .../worldState/worldState.json | 48 ------------- 2 files changed, 54 insertions(+), 61 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 2ca13620..e1946f7a 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -4,14 +4,7 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { - ICalendarDay, - ICalendarSeason, - ILiteSortie, - ISeasonChallenge, - ISortie, - IWorldState -} from "../types/worldStateTypes"; +import { ICalendarDay, ICalendarSeason, ILiteSortie, ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -174,7 +167,47 @@ const getSortieTime = (day: number): number => { return dayStart + (isDst ? 16 : 17) * 3600000; }; -const pushSortieIfRelevant = (out: ISortie[], day: number): void => { +const pushSyndicateMissions = ( + worldState: IWorldState, + day: number, + seed: number, + idSuffix: string, + syndicateTag: string +): void => { + const nodeOptions: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.name.indexOf("Archwing") == -1 && // no archwing + value.systemIndex != 23 && // no 1999 stuff + value.missionIndex != 10 && // Exclude MT_PVP (for relays) + value.missionIndex != 23 && // no junctions + value.missionIndex <= 28 // no railjack or some such + ) { + nodeOptions.push(key); + } + } + + const rng = new CRng(seed); + const nodes: string[] = []; + for (let i = 0; i != 6; ++i) { + const index = rng.randomInt(0, nodeOptions.length - 1); + nodes.push(nodeOptions[index]); + nodeOptions.splice(index, 1); + } + + const dayStart = getSortieTime(day); + const dayEnd = getSortieTime(day + 1); + worldState.SyndicateMissions.push({ + _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + idSuffix }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Tag: syndicateTag, + Seed: seed, + Nodes: nodes + }); +}; + +const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { const dayStart = getSortieTime(day); if (!isBeforeNextExpectedWorldStateRefresh(dayStart)) { return; @@ -231,6 +264,7 @@ const pushSortieIfRelevant = (out: ISortie[], day: number): void => { value.name.indexOf("Archwing") == -1 && value.missionIndex != 0 && // Exclude MT_ASSASSINATION value.missionIndex != 5 && // Exclude MT_CAPTURE + value.missionIndex != 10 && // Exclude MT_PVP (for relays) value.missionIndex != 21 && // Exclude MT_PURIFY value.missionIndex != 22 && // Exclude MT_ARENA value.missionIndex != 23 && // Exclude MT_JUNCTION @@ -292,7 +326,7 @@ const pushSortieIfRelevant = (out: ISortie[], day: number): void => { missionTypes.add(missionType); } - out.push({ + worldState.Sorties.push({ _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, @@ -301,6 +335,13 @@ const pushSortieIfRelevant = (out: ISortie[], day: number): void => { Boss: boss, Variants: selectedNodes }); + + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate"); }; const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => @@ -953,9 +994,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); } - // Sortie cycling every day - pushSortieIfRelevant(worldState.Sorties, day - 1); - pushSortieIfRelevant(worldState.Sorties, day); + // Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST) + pushSortieIfRelevant(worldState, day - 1); + pushSortieIfRelevant(worldState, day); // Archon Hunt cycling every week worldState.LiteSorties.push(getLiteSortie(week)); diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index dd76dd56..9adb9556 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -63,22 +63,6 @@ } ], "SyndicateMissions": [ - { - "_id": { "$oid": "663a4fc5ba6f84724fa48049" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "ArbitersSyndicate", - "Seed": 24491, - "Nodes": ["SolNode223", "SolNode89", "SolNode146", "SolNode212", "SolNode167", "SolNode48", "SolNode78"] - }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa4804a" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "CephalonSudaSyndicate", - "Seed": 12770, - "Nodes": ["SolNode36", "SolNode59", "SettlementNode12", "SolNode61", "SolNode12", "SolNode138", "SolNode72"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa4804c" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -103,14 +87,6 @@ "Seed": 50102, "Nodes": [] }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa4804e" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "NewLokaSyndicate", - "Seed": 16064, - "Nodes": ["SolNode101", "SolNode224", "SolNode205", "SettlementNode2", "SolNode171", "SolNode188", "SolNode75"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa4804f" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -119,14 +95,6 @@ "Seed": 77721, "Nodes": [] }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa48050" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "PerrinSyndicate", - "Seed": 9940, - "Nodes": ["SolNode39", "SolNode14", "SolNode203", "SolNode100", "SolNode130", "SolNode64", "SettlementNode15"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa48052" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -255,14 +223,6 @@ "Seed": 67257, "Nodes": [] }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa4805e" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "RedVeilSyndicate", - "Seed": 46649, - "Nodes": ["SolNode226", "SolNode79", "SolNode216", "SettlementNode11", "SolNode56", "SolNode41", "SolNode23"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa48060" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -270,14 +230,6 @@ "Tag": "VoxSyndicate", "Seed": 77972, "Nodes": [] - }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa48061" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "SteelMeridianSyndicate", - "Seed": 42366, - "Nodes": ["SolNode27", "SolNode107", "SolNode214", "SettlementNode1", "SolNode177", "SolNode141", "SolNode408"] } ], "ActiveMissions": [ From daacbf6f7b2deeb3aeb47b2b1eb70ce1efc9048c Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:00:58 -0700 Subject: [PATCH 571/776] fix(webui): add `exalted` array for KubrowPets ItemLists (#1782) Closes #1770 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1782 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/custom/getItemListsController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 1c7d95f4..1ff78b62 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -69,7 +69,8 @@ const getItemListsController: RequestHandler = (req, response) => { if (item.productCategory != "SpecialItems") { res[item.productCategory].push({ uniqueName, - name: getString(item.name, lang) + name: getString(item.name, lang), + exalted: item.exalted }); } } From e17d43dcb6eef137adb60a10d1b6b1032e5de059 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:20:50 -0700 Subject: [PATCH 572/776] chore: fix slotNames duplication (#1798) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1798 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/importService.ts | 16 ++-------------- src/types/purchaseTypes.ts | 1 + 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/services/importService.ts b/src/services/importService.ts index ea03047b..cde182cc 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -37,6 +37,7 @@ import { } from "../types/inventoryTypes/inventoryTypes"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { ILoadoutConfigDatabase, ILoadoutDatabase } from "../types/saveLoadoutTypes"; +import { slotNames } from "../types/purchaseTypes"; const convertDate = (value: IMongoDate): Date => { return new Date(parseInt(value.$date.$numberLong)); @@ -212,20 +213,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< replaceArray(db[key], client[key].map(convertOperatorConfig)); } } - for (const key of [ - "SuitBin", - "WeaponBin", - "SentinelBin", - "SpaceSuitBin", - "SpaceWeaponBin", - "PvpBonusLoadoutBin", - "PveBonusLoadoutBin", - "RandomModBin", - "MechBin", - "CrewMemberBin", - "OperatorAmpBin", - "CrewShipSalvageBin" - ] as const) { + for (const key of slotNames) { if (client[key] !== undefined) { replaceSlots(db[key], client[key]); } diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 3600e6b7..8cb92ccc 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -103,6 +103,7 @@ export const slotNames = [ "WeaponBin", "MechBin", "PveBonusLoadoutBin", + "PvpBonusLoadoutBin", "SentinelBin", "SpaceSuitBin", "SpaceWeaponBin", From 146dbd1b89f89224adc72b1e39da8d6060ba741e Mon Sep 17 00:00:00 2001 From: hxedcl Date: Tue, 22 Apr 2025 18:52:46 -0700 Subject: [PATCH 573/776] chore(webui): update to Spanish translation (#1803) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1803 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 222534cd..7f8e462c 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -60,7 +60,7 @@ dict = { login_emailLabel: `Dirección de correo electrónico`, login_passwordLabel: `Contraseña`, login_loginButton: `Iniciar sesión`, - login_registerButton: `[UNTRANSLATED] Register`, + login_registerButton: `Registrarse`, navbar_logout: `Cerrar sesión`, navbar_renameAccount: `Renombrar cuenta`, navbar_deleteAccount: `Eliminar cuenta`, From 570c6fe0d1fec4d17df1b1731798910c6edfee2a Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 23 Apr 2025 11:20:25 -0700 Subject: [PATCH 574/776] chore(webui): update German translation (#1806) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1806 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index b8725e00..5dac7438 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -60,7 +60,7 @@ dict = { login_emailLabel: `E-Mail-Adresse`, login_passwordLabel: `Passwort`, login_loginButton: `Anmelden`, - login_registerButton: `[UNTRANSLATED] Register`, + login_registerButton: `Registrieren`, navbar_logout: `Abmelden`, navbar_renameAccount: `Account umbenennen`, navbar_deleteAccount: `Account löschen`, @@ -116,7 +116,7 @@ dict = { mods_rivens: `Rivens`, mods_mods: `Mods`, mods_bulkAddMods: `Fehlende Mods hinzufügen`, - mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, + mods_removeUnranked: `Mods ohne Rang entfernen`, cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge |DISPLAYNAME| zu administratorNames in der config.json hinzu.`, cheats_server: `Server`, cheats_skipTutorial: `Tutorial überspringen`, From 64290b72c0993079efd980cab22db6812e65cad5 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Wed, 23 Apr 2025 11:20:40 -0700 Subject: [PATCH 575/776] chore(webui): update to Spanish translation (#1809) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1809 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 7f8e462c..c52b68e4 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -116,7 +116,7 @@ dict = { mods_rivens: `Agrietados`, mods_mods: `Mods`, mods_bulkAddMods: `Agregar mods faltantes`, - mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, + mods_removeUnranked: `Quitar mods sin rango`, cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega |DISPLAYNAME| a administratorNames en el archivo config.json.`, cheats_server: `Servidor`, cheats_skipTutorial: `Omitir tutorial`, From ce5b0fc9e29411549382a5effce9eab361a3caf6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:35:29 -0700 Subject: [PATCH 576/776] fix: limit MT_LANDSCAPE sortie missions to PoE (#1790) Closes #1789 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1790 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index e1946f7a..284ad6ca 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -268,7 +268,8 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { value.missionIndex != 21 && // Exclude MT_PURIFY value.missionIndex != 22 && // Exclude MT_ARENA value.missionIndex != 23 && // Exclude MT_JUNCTION - value.missionIndex <= 28 + (value.missionIndex != 28 || value.systemIndex == 2) && // MT_LANDSCAPE only on Earth + value.missionIndex < 29 ) { if (!availableMissionIndexes.includes(value.missionIndex)) { availableMissionIndexes.push(value.missionIndex); From 15aaa28a4f674ad92d0c0913d38cd1adf73d21d6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:35:57 -0700 Subject: [PATCH 577/776] feat: conquest progression & rewards (#1791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1570 Co-authored-by: Jānis Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1791 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 +- package.json | 2 +- .../api/entratiLabConquestModeController.ts | 2 + .../api/missionInventoryUpdateController.ts | 16 +- src/services/missionInventoryUpdateService.ts | 207 +++++++++++++++++- src/types/missionTypes.ts | 19 ++ src/types/requestTypes.ts | 8 +- 7 files changed, 243 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 081c89a6..573523c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.56", + "warframe-public-export-plus": "^0.5.57", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.56", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.56.tgz", - "integrity": "sha512-px+J7tUm6fkSzwKkvL73ySQReDq9oM1UrHSLM3vbYGBvELM892iBgPYG45okIhScCSdwmmXTiWZTf4x/I4qiNQ==" + "version": "0.5.57", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.57.tgz", + "integrity": "sha512-CKbg7/2hSDH7I7yYSWwkrP4N2rEAEK1vNEuehj+RD9vMvl1c4u6klHLMwdh+ULxXiW4djWIlNIhs5bi/fm58Mg==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 15f4597c..330ed606 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.56", + "warframe-public-export-plus": "^0.5.57", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/entratiLabConquestModeController.ts b/src/controllers/api/entratiLabConquestModeController.ts index ae7b5845..e5b6c818 100644 --- a/src/controllers/api/entratiLabConquestModeController.ts +++ b/src/controllers/api/entratiLabConquestModeController.ts @@ -21,10 +21,12 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res) inventory.EntratiVaultCountResetDate = new Date(weekEnd); if (inventory.EntratiLabConquestUnlocked) { inventory.EntratiLabConquestUnlocked = 0; + inventory.EntratiLabConquestCacheScoreMission = 0; inventory.EntratiLabConquestActiveFrameVariants = []; } if (inventory.EchoesHexConquestUnlocked) { inventory.EchoesHexConquestUnlocked = 0; + inventory.EchoesHexConquestCacheScoreMission = 0; inventory.EchoesHexConquestActiveFrameVariants = []; inventory.EchoesHexConquestActiveStickers = []; } diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index ead3b56e..bde1c3cf 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -6,6 +6,7 @@ import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/mi import { getInventory } from "@/src/services/inventoryService"; import { getInventoryResponse } from "./inventoryController"; import { logger } from "@/src/utils/logger"; +import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes"; /* **** INPUT **** @@ -71,8 +72,14 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) return; } - const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } = - await addMissionRewards(inventory, missionReport, firstCompletion); + const { + MissionRewards, + inventoryChanges, + credits, + AffiliationMods, + SyndicateXPItemReward, + ConquestCompletedMissionsCount + } = await addMissionRewards(inventory, missionReport, firstCompletion); await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); @@ -86,8 +93,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) ...inventoryUpdates, //FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed. SyndicateXPItemReward, - AffiliationMods - }); + AffiliationMods, + ConquestCompletedMissionsCount + } satisfies IMissionInventoryUpdateResponse); }; /* diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 46d039c1..f03dda81 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -43,7 +43,7 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { handleStoreItemAcquisition } from "./purchaseService"; -import { IMissionReward } from "../types/missionTypes"; +import { IMissionCredits, IMissionReward } from "../types/missionTypes"; import { crackRelic } from "@/src/helpers/relicHelper"; import { createMessage } from "./inboxService"; import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json"; @@ -586,8 +586,140 @@ interface AddMissionRewardsReturnType { credits?: IMissionCredits; AffiliationMods?: IAffiliationMods[]; SyndicateXPItemReward?: number; + ConquestCompletedMissionsCount?: number; } +interface IConquestReward { + at: number; + pool: IRngResult[]; +} + +const labConquestRewards: IConquestReward[] = [ + { + at: 5, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 10, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 15, + pool: [ + { + type: "/Lotus/StoreItems/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle", + itemCount: 3, + probability: 1 + } + ] + }, + { + at: 20, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 28, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints", + itemCount: 20, + probability: 1 + } + ] + }, + { + at: 31, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 34, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestArcaneRewards" + ][0] as IRngResult[] + }, + { + at: 37, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints", + itemCount: 50, + probability: 1 + } + ] + } +]; + +const hexConquestRewards: IConquestReward[] = [ + { + at: 5, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 10, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 15, + pool: [ + { + type: "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea", + itemCount: 1, + probability: 1 + } + ] + }, + { + at: 20, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 28, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks", + itemCount: 6, + probability: 1 + } + ] + }, + { + at: 31, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 34, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestArcaneRewards" + ][0] as IRngResult[] + }, + { + at: 37, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks", + itemCount: 9, + probability: 1 + } + ] + } +]; + //TODO: return type of partial missioninventoryupdate response export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, @@ -620,6 +752,7 @@ export const addMissionRewards = async ( const inventoryChanges: IInventoryChanges = {}; const AffiliationMods: IAffiliationMods[] = []; let SyndicateXPItemReward; + let ConquestCompletedMissionsCount; let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display @@ -691,6 +824,62 @@ export const addMissionRewards = async ( }); } + if (rewardInfo.ConquestCompleted !== undefined) { + let score = 1; + if (rewardInfo.ConquestHardModeActive === 1) score += 3; + + if (rewardInfo.ConquestPersonalModifiersActive !== undefined) + score += rewardInfo.ConquestPersonalModifiersActive; + if (rewardInfo.ConquestEquipmentSuggestionsFulfilled !== undefined) + score += rewardInfo.ConquestEquipmentSuggestionsFulfilled; + + score *= rewardInfo.ConquestCompleted + 1; + + if (rewardInfo.ConquestCompleted == 2 && rewardInfo.ConquestHardModeActive === 1) score += 1; + + logger.debug(`completed conquest mission ${rewardInfo.ConquestCompleted + 1} for a score of ${score}`); + + const conquestType = rewardInfo.ConquestType; + const conquestNode = + conquestType == "HexConquest" ? "EchoesHexConquestHardModeUnlocked" : "EntratiLabConquestHardModeUnlocked"; + if (score >= 25 && inventory.NodeIntrosCompleted.indexOf(conquestNode) == -1) + inventory.NodeIntrosCompleted.push(conquestNode); + + if (conquestType == "HexConquest") { + inventory.EchoesHexConquestCacheScoreMission ??= 0; + if (score > inventory.EchoesHexConquestCacheScoreMission) { + for (const reward of hexConquestRewards) { + if (score >= reward.at && inventory.EchoesHexConquestCacheScoreMission < reward.at) { + const rolled = getRandomReward(reward.pool)!; + logger.debug(`rolled hex conquest reward for reaching ${reward.at} points`, rolled); + MissionRewards.push({ + StoreItem: rolled.type, + ItemCount: rolled.itemCount + }); + } + } + inventory.EchoesHexConquestCacheScoreMission = score; + } + } else { + inventory.EntratiLabConquestCacheScoreMission ??= 0; + if (score > inventory.EntratiLabConquestCacheScoreMission) { + for (const reward of labConquestRewards) { + if (score >= reward.at && inventory.EntratiLabConquestCacheScoreMission < reward.at) { + const rolled = getRandomReward(reward.pool)!; + logger.debug(`rolled lab conquest reward for reaching ${reward.at} points`, rolled); + MissionRewards.push({ + StoreItem: rolled.type, + ItemCount: rolled.itemCount + }); + } + } + inventory.EntratiLabConquestCacheScoreMission = score; + } + } + + ConquestCompletedMissionsCount = rewardInfo.ConquestCompleted == 2 ? 0 : rewardInfo.ConquestCompleted + 1; + } + for (const reward of MissionRewards) { const inventoryChange = await handleStoreItemAcquisition( reward.StoreItem, @@ -882,16 +1071,16 @@ export const addMissionRewards = async ( } } - return { inventoryChanges, MissionRewards, credits, AffiliationMods, SyndicateXPItemReward }; + return { + inventoryChanges, + MissionRewards, + credits, + AffiliationMods, + SyndicateXPItemReward, + ConquestCompletedMissionsCount + }; }; -interface IMissionCredits { - MissionCredits: number[]; - CreditBonus: number[]; - TotalCredits: number[]; - DailyMissionBonus?: boolean; -} - //creditBonus is not entirely accurate. //TODO: consider ActiveBoosters export const addCredits = ( diff --git a/src/types/missionTypes.ts b/src/types/missionTypes.ts index c8cab373..be1d08bc 100644 --- a/src/types/missionTypes.ts +++ b/src/types/missionTypes.ts @@ -1,3 +1,5 @@ +import { IAffiliationMods, IInventoryChanges } from "./purchaseTypes"; + export const inventoryFields = ["RawUpgrades", "MiscItems", "Consumables", "Recipes"] as const; export type IInventoryFieldType = (typeof inventoryFields)[number]; @@ -11,3 +13,20 @@ export interface IMissionReward { FromEnemyCache?: boolean; IsStrippedItem?: boolean; } + +export interface IMissionCredits { + MissionCredits: number[]; + CreditBonus: number[]; + TotalCredits: number[]; + DailyMissionBonus?: boolean; +} + +export interface IMissionInventoryUpdateResponse extends Partial { + ConquestCompletedMissionsCount?: number; + InventoryJson?: string; + MissionRewards?: IMissionReward[]; + InventoryChanges?: IInventoryChanges; + FusionPoints?: number; + SyndicateXPItemReward?: number; + AffiliationMods?: IAffiliationMods[]; +} diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index b81fa064..441bf03e 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -125,6 +125,7 @@ export type IMissionInventoryUpdateRequest = { wagerTier?: number; // the index creditsFee?: number; // the index InvasionProgress?: IInvasionProgressClient[]; + ConquestMissionsCompleted?: number; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; @@ -150,7 +151,12 @@ export interface IRewardInfo { PurgatoryRewardQualifications?: string; rewardSeed?: number | bigint; periodicMissionTag?: string; - + ConquestType?: string; + ConquestCompleted?: number; + ConquestEquipmentSuggestionsFulfilled?: number; + ConquestPersonalModifiersActive?: number; + ConquestStickersActive?: number; + ConquestHardModeActive?: number; // for bounties, only EOM_AFK and node are given from above, plus: JobTier?: number; jobId?: string; From f3601ec43ee6248e6ee464c5ccd89792f9353860 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:36:19 -0700 Subject: [PATCH 578/776] chore: allow MT_CAPTURE for sorties (#1792) e.g. `SolNode1` is a capture mission but it should still be a valid node for sorties. Not that the mission will actually be a capture. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1792 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 284ad6ca..0b58e1f7 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -263,7 +263,6 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && value.name.indexOf("Archwing") == -1 && value.missionIndex != 0 && // Exclude MT_ASSASSINATION - value.missionIndex != 5 && // Exclude MT_CAPTURE value.missionIndex != 10 && // Exclude MT_PVP (for relays) value.missionIndex != 21 && // Exclude MT_PURIFY value.missionIndex != 22 && // Exclude MT_ARENA From d6750cd84ba47db8264c78172e27e4ccde5ff977 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:36:32 -0700 Subject: [PATCH 579/776] chore: provide tileset for sortie missions (#1793) Closes #1788 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1793 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 33 +++- src/types/worldStateTypes.ts | 7 + .../worldState/sortieTilesets.json | 175 ++++++++++++++++++ 3 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 static/fixed_responses/worldState/sortieTilesets.json diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 0b58e1f7..f9d727a9 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1,10 +1,18 @@ import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; +import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json"; import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { ICalendarDay, ICalendarSeason, ILiteSortie, ISeasonChallenge, IWorldState } from "../types/worldStateTypes"; +import { + ICalendarDay, + ICalendarSeason, + ILiteSortie, + ISeasonChallenge, + ISortieMission, + IWorldState +} from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -277,7 +285,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { } } - const selectedNodes: { missionType: string; modifierType: string; node: string }[] = []; + const selectedNodes: ISortieMission[] = []; const missionTypes = new Set(); for (let i = 0; i < 3; i++) { @@ -298,11 +306,21 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { const modifierType = rng.randomElement(filteredModifiers); if (boss == "SORTIE_BOSS_PHORID") { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node }); + selectedNodes.push({ + missionType: "MT_ASSASSINATION", + modifierType, + node, + tileset: sortieTilesets[node as keyof typeof sortieTilesets] + }); nodes.splice(randomIndex, 1); continue; } else if (sortieBossNode[boss]) { - selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] }); + selectedNodes.push({ + missionType: "MT_ASSASSINATION", + modifierType, + node: sortieBossNode[boss], + tileset: sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] + }); continue; } } @@ -321,7 +339,12 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { const modifierType = rng.randomElement(filteredModifiers); - selectedNodes.push({ missionType, modifierType, node }); + selectedNodes.push({ + missionType, + modifierType, + node, + tileset: sortieTilesets[node as keyof typeof sortieTilesets] + }); nodes.splice(randomIndex, 1); missionTypes.add(missionType); } diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 67178c73..835792b8 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -97,6 +97,13 @@ export interface ISortie { }[]; } +export interface ISortieMission { + missionType: string; + modifierType: string; + node: string; + tileset: string; +} + export interface ILiteSortie { _id: IOid; Activation: IMongoDate; diff --git a/static/fixed_responses/worldState/sortieTilesets.json b/static/fixed_responses/worldState/sortieTilesets.json new file mode 100644 index 00000000..d2965c09 --- /dev/null +++ b/static/fixed_responses/worldState/sortieTilesets.json @@ -0,0 +1,175 @@ +{ + "SettlementNode1": "CorpusShipTileset", + "SettlementNode11": "CorpusShipTileset", + "SettlementNode12": "CorpusShipTileset", + "SettlementNode14": "CorpusShipTileset", + "SettlementNode15": "CorpusShipTileset", + "SettlementNode2": "CorpusShipTileset", + "SettlementNode20": "CorpusShipTileset", + "SettlementNode3": "CorpusShipTileset", + "SolNode1": "CorpusOutpostTileset", + "SolNode10": "CorpusGasCityTileset", + "SolNode100": "CorpusGasCityTileset", + "SolNode101": "CorpusOutpostTileset", + "SolNode102": "CorpusShipTileset", + "SolNode103": "GrineerAsteroidTileset", + "SolNode104": "CorpusShipTileset", + "SolNode105": "GrineerOceanTilesetAnywhere", + "SolNode106": "GrineerSettlementTileset", + "SolNode107": "CorpusOutpostTileset", + "SolNode108": "GrineerAsteroidTileset", + "SolNode109": "CorpusOutpostTileset", + "SolNode11": "GrineerSettlementTileset", + "SolNode113": "GrineerSettlementTileset", + "SolNode118": "CorpusShipTileset", + "SolNode119": "GrineerAsteroidTileset", + "SolNode12": "GrineerAsteroidTileset", + "SolNode121": "CorpusGasCityTileset", + "SolNode122": "GrineerOceanTileset", + "SolNode123": "CorpusShipTileset", + "SolNode125": "CorpusGasCityTileset", + "SolNode126": "CorpusGasCityTileset", + "SolNode127": "CorpusShipTileset", + "SolNode128": "CorpusOutpostTileset", + "SolNode130": "GrineerAsteroidTileset", + "SolNode131": "GrineerShipyardsTileset", + "SolNode132": "GrineerShipyardsTileset", + "SolNode135": "GrineerGalleonTileset", + "SolNode137": "GrineerShipyardsTileset", + "SolNode138": "GrineerShipyardsTileset", + "SolNode139": "GrineerShipyardsTileset", + "SolNode14": "CorpusIcePlanetTilesetCaves", + "SolNode140": "GrineerShipyardsTileset", + "SolNode141": "GrineerShipyardsTileset", + "SolNode144": "GrineerShipyardsTileset", + "SolNode146": "GrineerAsteroidTileset", + "SolNode147": "GrineerShipyardsTileset", + "SolNode149": "GrineerShipyardsTileset", + "SolNode15": "GrineerGalleonTileset", + "SolNode16": "GrineerSettlementTileset", + "SolNode162": "InfestedCorpusShipTileset", + "SolNode164": "InfestedCorpusShipTileset", + "SolNode166": "InfestedCorpusShipTileset", + "SolNode17": "CorpusShipTileset", + "SolNode171": "InfestedCorpusShipTileset", + "SolNode172": "CorpusShipTileset", + "SolNode173": "InfestedCorpusShipTileset", + "SolNode175": "InfestedCorpusShipTileset", + "SolNode177": "GrineerGalleonTileset", + "SolNode18": "GrineerAsteroidTileset", + "SolNode181": "GrineerAsteroidTileset", + "SolNode184": "GrineerGalleonTileset", + "SolNode185": "GrineerGalleonTileset", + "SolNode187": "GrineerAsteroidTileset", + "SolNode188": "GrineerGalleonTileset", + "SolNode189": "GrineerGalleonTileset", + "SolNode19": "GrineerAsteroidTileset", + "SolNode191": "GrineerShipyardsTileset", + "SolNode193": "GrineerAsteroidTileset", + "SolNode195": "GrineerGalleonTileset", + "SolNode196": "GrineerGalleonTileset", + "SolNode2": "CorpusOutpostTileset", + "SolNode20": "GrineerGalleonTileset", + "SolNode203": "CorpusIcePlanetTileset", + "SolNode205": "CorpusIcePlanetTileset", + "SolNode209": "CorpusIcePlanetTileset", + "SolNode21": "CorpusOutpostTileset", + "SolNode210": "CorpusIcePlanetTileset", + "SolNode211": "CorpusIcePlanetTileset", + "SolNode212": "CorpusIcePlanetTileset", + "SolNode214": "CorpusIcePlanetTileset", + "SolNode215": "CorpusShipTileset", + "SolNode216": "CorpusIcePlanetTileset", + "SolNode217": "CorpusIcePlanetTileset", + "SolNode22": "CorpusOutpostTileset", + "SolNode220": "CorpusIcePlanetTileset", + "SolNode223": "GrineerAsteroidTileset", + "SolNode224": "GrineerGalleonTileset", + "SolNode225": "GrineerGalleonTileset", + "SolNode226": "GrineerGalleonTileset", + "SolNode228": "EidolonTileset", + "SolNode23": "CorpusShipTileset", + "SolNode24": "GrineerForestTileset", + "SolNode25": "CorpusGasCityTileset", + "SolNode26": "GrineerForestTileset", + "SolNode30": "GrineerSettlementTileset", + "SolNode300": "OrokinMoonTilesetGrineer", + "SolNode301": "OrokinMoonTilesetGrineer", + "SolNode302": "OrokinMoonTilesetCorpus", + "SolNode304": "OrokinMoonTilesetCorpus", + "SolNode305": "OrokinMoonTilesetGrineer", + "SolNode306": "OrokinMoonTilesetCorpus", + "SolNode307": "OrokinMoonTilesetCorpus", + "SolNode308": "OrokinMoonTilesetCorpus", + "SolNode31": "GrineerGalleonTileset", + "SolNode32": "GrineerGalleonTileset", + "SolNode36": "GrineerSettlementTileset", + "SolNode38": "CorpusOutpostTileset", + "SolNode39": "GrineerForestTileset", + "SolNode4": "CorpusShipTileset", + "SolNode400": "OrokinVoidTileset", + "SolNode401": "OrokinVoidTileset", + "SolNode402": "OrokinVoidTileset", + "SolNode403": "OrokinVoidTileset", + "SolNode404": "OrokinVoidTileset", + "SolNode405": "OrokinVoidTileset", + "SolNode406": "OrokinVoidTileset", + "SolNode407": "OrokinVoidTileset", + "SolNode408": "OrokinVoidTileset", + "SolNode409": "OrokinVoidTileset", + "SolNode41": "GrineerSettlementTileset", + "SolNode410": "OrokinVoidTileset", + "SolNode412": "OrokinVoidTileset", + "SolNode42": "GrineerGalleonTileset", + "SolNode43": "CorpusOutpostTileset", + "SolNode45": "GrineerSettlementTileset", + "SolNode46": "GrineerSettlementTileset", + "SolNode48": "CorpusOutpostTileset", + "SolNode49": "CorpusShipTileset", + "SolNode50": "GrineerAsteroidTileset", + "SolNode51": "CorpusOutpostTileset", + "SolNode53": "CorpusGasCityTileset", + "SolNode56": "CorpusShipTileset", + "SolNode57": "CorpusOutpostTileset", + "SolNode58": "GrineerSettlementTileset", + "SolNode59": "GrineerForestTileset", + "SolNode6": "CorpusOutpostTileset", + "SolNode61": "CorpusShipTileset", + "SolNode62": "CorpusIcePlanetTilesetCaves", + "SolNode64": "GrineerOceanTileset", + "SolNode66": "CorpusOutpostTileset", + "SolNode67": "GrineerAsteroidTileset", + "SolNode68": "GrineerGalleonTileset", + "SolNode70": "GrineerGalleonTileset", + "SolNode706": "OrokinDerelictTileset", + "SolNode707": "OrokinDerelictTileset", + "SolNode708": "OrokinDerelictTileset", + "SolNode709": "OrokinDerelictTileset", + "SolNode710": "OrokinDerelictTileset", + "SolNode711": "OrokinDerelictTileset", + "SolNode712": "OrokinDerelictTileset", + "SolNode713": "OrokinDerelictTileset", + "SolNode72": "CorpusOutpostTileset", + "SolNode73": "CorpusGasCityTileset", + "SolNode74": "CorpusGasCityTileset", + "SolNode741": "GrineerFortressTileset", + "SolNode742": "GrineerFortressTileset", + "SolNode743": "GrineerFortressTileset", + "SolNode744": "GrineerFortressTileset", + "SolNode745": "GrineerFortressTileset", + "SolNode746": "GrineerFortressTileset", + "SolNode747": "GrineerFortressTileset", + "SolNode748": "GrineerFortressTileset", + "SolNode75": "GrineerForestTileset", + "SolNode76": "CorpusShipTileset", + "SolNode78": "CorpusShipTileset", + "SolNode79": "GrineerForestTileset", + "SolNode81": "CorpusShipTileset", + "SolNode82": "GrineerGalleonTileset", + "SolNode84": "CorpusIcePlanetTilesetCaves", + "SolNode88": "CorpusShipTileset", + "SolNode93": "GrineerAsteroidTileset", + "SolNode96": "GrineerGalleonTileset", + "SolNode97": "CorpusGasCityTileset", + "SolNode99": "GrineerSettlementTileset" +} From 26d644a982c2ea4b29ff7ea09de3f04efc4a7e93 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:36:57 -0700 Subject: [PATCH 580/776] feat: handle scale for the dojo decos that need it (#1795) Closes #1785 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1795 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/placeDecoInComponentController.ts | 4 +++- src/models/guildModel.ts | 1 + src/services/guildService.ts | 1 + src/types/guildTypes.ts | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts index cf50a90b..a45806a8 100644 --- a/src/controllers/api/placeDecoInComponentController.ts +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -37,6 +37,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = const deco = component.Decos.find(x => x._id.equals(request.MoveId))!; deco.Pos = request.Pos; deco.Rot = request.Rot; + deco.Scale = request.Scale; } else { const deco = component.Decos[ @@ -45,6 +46,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) = Type: request.Type, Pos: request.Pos, Rot: request.Rot, + Scale: request.Scale, Name: request.Name, Sockets: request.Sockets }) - 1 @@ -113,9 +115,9 @@ interface IPlaceDecoInComponentRequest { Type: string; Pos: number[]; Rot: number[]; + Scale?: number; Name?: string; Sockets?: number; - Scale?: number; // only provided alongside MoveId and seems to always be 1 MoveId?: string; ShipDeco?: boolean; VaultDeco?: boolean; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index ffb5dac5..15a4c5a6 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -23,6 +23,7 @@ const dojoDecoSchema = new Schema({ Type: String, Pos: [Number], Rot: [Number], + Scale: Number, Name: String, Sockets: Number, RegularCredits: Number, diff --git a/src/services/guildService.ts b/src/services/guildService.ts index d409654d..aba101e6 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -222,6 +222,7 @@ export const getDojoClient = async ( Type: deco.Type, Pos: deco.Pos, Rot: deco.Rot, + Scale: deco.Scale, Name: deco.Name, Sockets: deco.Sockets, PictureFrameInfo: deco.PictureFrameInfo diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 59e7a3a0..dc71e3ec 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -206,6 +206,7 @@ export interface IDojoDecoClient { Type: string; Pos: number[]; Rot: number[]; + Scale?: number; Name?: string; // for teleporters Sockets?: number; RegularCredits?: number; From 7a8b12b372e4e0caf01f82082a3aa1d5779a0e3b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:37:10 -0700 Subject: [PATCH 581/776] chore: cap FusionPoints balance at 2147483647 (#1797) Same idea as with typeCountSchema. The game needs to be able to store these safely in an i32 on the C++ side. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1797 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/claimLibraryDailyTaskRewardController.ts | 4 ++-- .../api/contributeGuildClassController.ts | 4 ++-- src/controllers/api/sellController.ts | 5 +++-- src/controllers/custom/addCurrencyController.ts | 10 +++++++--- src/services/inventoryService.ts | 11 ++++++++++- src/services/missionInventoryUpdateService.ts | 13 +++++++------ 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/claimLibraryDailyTaskRewardController.ts b/src/controllers/api/claimLibraryDailyTaskRewardController.ts index 6d8e1d41..3d582e46 100644 --- a/src/controllers/api/claimLibraryDailyTaskRewardController.ts +++ b/src/controllers/api/claimLibraryDailyTaskRewardController.ts @@ -1,4 +1,4 @@ -import { getInventory } from "@/src/services/inventoryService"; +import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; @@ -17,7 +17,7 @@ export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, } syndicate.Standing += rewardStanding; - inventory.FusionPoints += 80 * rewardQuantity; + addFusionPoints(inventory, 80 * rewardQuantity); await inventory.save(); res.json({ diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts index c4fa7280..865eb868 100644 --- a/src/controllers/api/contributeGuildClassController.ts +++ b/src/controllers/api/contributeGuildClassController.ts @@ -2,7 +2,7 @@ import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Guild } from "@/src/models/guildModel"; import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService"; -import { getInventory } from "@/src/services/inventoryService"; +import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; import { Types } from "mongoose"; @@ -36,7 +36,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) = // Either way, endo is given to the contributor. const inventory = await getInventory(accountId, "FusionPoints"); - inventory.FusionPoints += guild.CeremonyEndo!; + addFusionPoints(inventory, guild.CeremonyEndo!); await inventory.save(); res.json({ diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index 138cd3b4..4a2025c1 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -8,7 +8,8 @@ import { addConsumables, freeUpSlot, combineInventoryChanges, - addCrewShipRawSalvage + addCrewShipRawSalvage, + addFusionPoints } from "@/src/services/inventoryService"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { ExportDojoRecipes } from "warframe-public-export-plus"; @@ -69,7 +70,7 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.SellCurrency == "SC_RegularCredits") { inventory.RegularCredits += payload.SellPrice; } else if (payload.SellCurrency == "SC_FusionPoints") { - inventory.FusionPoints += payload.SellPrice; + addFusionPoints(inventory, payload.SellPrice); } else if (payload.SellCurrency == "SC_PrimeBucks") { addMiscItems(inventory, [ { diff --git a/src/controllers/custom/addCurrencyController.ts b/src/controllers/custom/addCurrencyController.ts index 63dedb6f..be14b8a3 100644 --- a/src/controllers/custom/addCurrencyController.ts +++ b/src/controllers/custom/addCurrencyController.ts @@ -1,12 +1,16 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; export const addCurrencyController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const request = req.body as IAddCurrencyRequest; - inventory[request.currency] += request.delta; + const inventory = await getInventory(accountId, request.currency); + if (request.currency == "FusionPoints") { + addFusionPoints(inventory, request.delta); + } else { + inventory[request.currency] += request.delta; + } await inventory.save(); res.end(); }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9e89dcfe..51312029 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -580,7 +580,7 @@ export const addItem = async ( } if (typeName in ExportFusionBundles) { const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity; - inventory.FusionPoints += fusionPointsTotal; + addFusionPoints(inventory, fusionPointsTotal); return { FusionPoints: fusionPointsTotal }; @@ -1069,6 +1069,15 @@ export const updateCurrency = ( return currencyChanges; }; +export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => { + if (inventory.FusionPoints + add > 2147483647) { + logger.warn(`capping FusionPoints balance at 2147483647`); + add = 2147483647 - inventory.FusionPoints; + } + inventory.FusionPoints += add; + return add; +}; + const standingLimitBinToInventoryKey: Record< Exclude, keyof IDailyAffiliations diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index f03dda81..05b94b0a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -19,6 +19,7 @@ import { addCrewShipRawSalvage, addEmailItem, addFocusXpIncreases, + addFusionPoints, addFusionTreasures, addGearExpByCategory, addItem, @@ -287,14 +288,14 @@ export const addMissionInventoryUpdates = async ( addShipDecorations(inventory, value); break; case "FusionBundles": { - let fusionPoints = 0; + let fusionPointsDelta = 0; for (const fusionBundle of value) { - const fusionPointsTotal = - ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount; - inventory.FusionPoints += fusionPointsTotal; - fusionPoints += fusionPointsTotal; + fusionPointsDelta += addFusionPoints( + inventory, + ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount + ); } - inventoryChanges.FusionPoints = fusionPoints; + inventoryChanges.FusionPoints = fusionPointsDelta; break; } case "EmailItems": { From 948104a9a6625cd63ae2e4326993d8a5d1ccaed2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:37:31 -0700 Subject: [PATCH 582/776] fix: "logged in elsewhere" when logging in on account created via webui (#1800) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1800 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 | 13 +++++++++++-- static/webui/script.js | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index f01c405a..565bebf0 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -21,7 +21,11 @@ export const loginController: RequestHandler = async (request, response) => { const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress; - if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { + if ( + !account && + ((config.autoCreateAccount && loginRequest.ClientType != "webui") || + loginRequest.ClientType == "webui-register") + ) { try { const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja"; @@ -37,7 +41,7 @@ export const loginController: RequestHandler = async (request, response) => { password: loginRequest.password, DisplayName: name, CountryCode: loginRequest.lang.toUpperCase(), - ClientType: loginRequest.ClientType, + ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, CrossPlatformAllowed: true, ForceLogoutVersion: 0, ConsentNeeded: false, @@ -59,6 +63,11 @@ export const loginController: RequestHandler = async (request, response) => { return; } + if (loginRequest.ClientType == "webui-register") { + response.status(400).json({ error: "account already exists" }); + return; + } + if (!isCorrectPassword(loginRequest.password, account.password)) { response.status(400).json({ error: "incorrect login data" }); return; diff --git a/static/webui/script.js b/static/webui/script.js index 7868e189..e8dce874 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -13,6 +13,7 @@ function doLogin() { } function loginFromLocalStorage() { + const isRegister = registerSubmit; doLoginRequest( data => { if (single.getCurrentPath() == "/webui/") { @@ -28,7 +29,7 @@ function loginFromLocalStorage() { }, () => { logout(); - alert("Login failed"); + alert(isRegister ? "Registration failed. Account already exists?" : "Login failed"); } ); } @@ -44,7 +45,7 @@ function doLoginRequest(succ_cb, fail_cb) { s: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==", // signature of some kind lang: "en", date: 1501230947855458660, // ??? - ClientType: registerSubmit ? "" : "webui", + ClientType: registerSubmit ? "webui-register" : "webui", PS: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==" // anti-cheat data }) }); From ada6a4bad045140e7d5a688859ad0fae391ad004 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:37:43 -0700 Subject: [PATCH 583/776] fix: occupy correct slot for arch-guns (#1801) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1801 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 51312029..c1f471a6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -532,7 +532,11 @@ export const addItem = async ( } return { ...inventoryChanges, - ...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase) + ...occupySlot( + inventory, + productCategoryToInventoryBin(weapon.productCategory) ?? InventorySlot.WEAPONS, + premiumPurchase + ) }; } else { // Modular weapon parts From a85539a686fd9c513a08d8ad6987f4561be95064 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:37:52 -0700 Subject: [PATCH 584/776] feat: set IsNew flag on new sentinels (#1802) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1802 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index c1f471a6..84984dc9 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -825,7 +825,8 @@ const addSentinel = ( const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined; const sentinelIndex = - inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features }) - 1; + inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features, IsNew: true }) - + 1; inventoryChanges.Sentinels ??= []; inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON()); From bb8596fa87064ae62f49ad47dbda9da6fc3f4877 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:38:04 -0700 Subject: [PATCH 585/776] fix(webui): use proper 'size' abbreviations for vallis & deimos fish (#1804) Closes #1763 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1804 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../custom/getItemListsController.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 1ff78b62..09e42dd2 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -115,12 +115,28 @@ const getItemListsController: RequestHandler = (req, response) => { let name = getString(item.name, lang); if ("dissectionParts" in item) { name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name); - if (uniqueName.indexOf("Large") != -1) { - name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang)); - } else if (uniqueName.indexOf("Medium") != -1) { - name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang)); + if (item.syndicateTag == "CetusSyndicate") { + if (uniqueName.indexOf("Large") != -1) { + name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang)); + } else if (uniqueName.indexOf("Medium") != -1) { + name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang)); + } else { + name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); + } } else { - name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); + if (uniqueName.indexOf("Large") != -1) { + name = name + .split("|FISH_SIZE|") + .join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryElderAbbrev", lang)); + } else if (uniqueName.indexOf("Medium") != -1) { + name = name + .split("|FISH_SIZE|") + .join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryMatureAbbrev", lang)); + } else { + name = name + .split("|FISH_SIZE|") + .join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryYoungAbbrev", lang)); + } } } if ( From eb594af9d8ff1dd44a22ee0df60fb45166f3b6a5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:40:45 -0700 Subject: [PATCH 586/776] chore: improve archwing mission detection (#1794) SettlementNode10 was not being excluded Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1794 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/helpers/nemesisHelpers.ts | 3 ++- src/services/worldStateService.ts | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index fff5e94e..ce4190fa 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -6,6 +6,7 @@ import { logger } from "../utils/logger"; import { IOid } from "../types/commonTypes"; import { Types } from "mongoose"; import { addMods } from "../services/inventoryService"; +import { isArchwingMission } from "../services/worldStateService"; export const getInfNodes = (faction: string, rank: number): IInfNode[] => { const infNodes = []; @@ -22,7 +23,7 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => { value.missionIndex != 42 && // not face off value.name.indexOf("1999NodeI") == -1 && // not stage defence value.name.indexOf("1999NodeJ") == -1 && // not lich bounty - value.name.indexOf("Archwing") == -1 + !isArchwingMission(value) ) { //console.log(dict_en[value.name]); infNodes.push({ Node: key, Influence: 1 }); diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index f9d727a9..18cc9d5c 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -4,7 +4,7 @@ import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; -import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; +import { eMissionType, ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, ICalendarSeason, @@ -185,7 +185,7 @@ const pushSyndicateMissions = ( const nodeOptions: string[] = []; for (const [key, value] of Object.entries(ExportRegions)) { if ( - value.name.indexOf("Archwing") == -1 && // no archwing + !isArchwingMission(value) && value.systemIndex != 23 && // no 1999 stuff value.missionIndex != 10 && // Exclude MT_PVP (for relays) value.missionIndex != 23 && // no junctions @@ -269,7 +269,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { if ( sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && - value.name.indexOf("Archwing") == -1 && + !isArchwingMission(value) && value.missionIndex != 0 && // Exclude MT_ASSASSINATION value.missionIndex != 10 && // Exclude MT_PVP (for relays) value.missionIndex != 21 && // Exclude MT_PURIFY @@ -1112,7 +1112,7 @@ export const getLiteSortie = (week: number): ILiteSortie => { value.systemIndex === systemIndex && value.factionIndex !== undefined && value.factionIndex < 2 && - value.name.indexOf("Archwing") == -1 && + !isArchwingMission(value) && value.missionIndex != 0 // Exclude MT_ASSASSINATION ) { nodes.push(key); @@ -1163,3 +1163,14 @@ export const getLiteSortie = (week: number): ILiteSortie => { ] }; }; + +export const isArchwingMission = (node: IRegion): boolean => { + if (node.name.indexOf("Archwing") != -1) { + return true; + } + // SettlementNode10 + if (node.missionIndex == 25) { + return true; + } + return false; +}; From f039998d713c9cce45926af18d353e1f06d416ea Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 00:33:20 +0200 Subject: [PATCH 587/776] chore: update PE+ to 0.5.58 --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/purchaseService.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 573523c5..45cca760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.57", + "warframe-public-export-plus": "^0.5.58", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.57", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.57.tgz", - "integrity": "sha512-CKbg7/2hSDH7I7yYSWwkrP4N2rEAEK1vNEuehj+RD9vMvl1c4u6klHLMwdh+ULxXiW4djWIlNIhs5bi/fm58Mg==" + "version": "0.5.58", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.58.tgz", + "integrity": "sha512-2G3tKcoblUl7S3Rkk5k/qH+VGZBUmU2QjtIrEO/Bt6UlgO83s648elkNdDKOLBKXnxIsa194nVwz+ci1K86sXg==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 330ed606..816118ff 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.57", + "warframe-public-export-plus": "^0.5.58", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 6c3b6d8c..6bf45e42 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -223,7 +223,7 @@ export const handlePurchase = async ( const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!]; const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem); if (offer) { - if (offer.credits) { + if (typeof offer.credits == "number") { combineInventoryChanges( purchaseResponse.InventoryChanges, updateCurrency(inventory, offer.credits, false) From 370f8c100880070631b6c379b02fa27da9715797 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 00:42:33 +0200 Subject: [PATCH 588/776] fix: getItemLists fixup --- src/controllers/custom/getItemListsController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 09e42dd2..43981011 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -58,6 +58,7 @@ const getItemListsController: RequestHandler = (req, response) => { res.OperatorAmps = []; res.QuestKeys = []; res.KubrowPets = []; + res.MoaPets = []; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ uniqueName, From 99e1a66da8ebc68dc31a5747e08a05a9c7ca1ac0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 00:46:33 +0200 Subject: [PATCH 589/776] chore: improve typings in getItemLists --- .../custom/getItemListsController.ts | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 43981011..a2c98c7d 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -32,6 +32,29 @@ interface ListedItem { parazon?: boolean; } +interface ItemLists { + archonCrystalUpgrades: Record; + uniqueLevelCaps: Record; + Suits: ListedItem[]; + LongGuns: ListedItem[]; + Melee: ListedItem[]; + ModularParts: ListedItem[]; + Pistols: ListedItem[]; + Sentinels: ListedItem[]; + SentinelWeapons: ListedItem[]; + SpaceGuns: ListedItem[]; + SpaceMelee: ListedItem[]; + SpaceSuits: ListedItem[]; + MechSuits: ListedItem[]; + miscitems: ListedItem[]; + Syndicates: ListedItem[]; + OperatorAmps: ListedItem[]; + QuestKeys: ListedItem[]; + KubrowPets: ListedItem[]; + MoaPets: ListedItem[]; + mods: ListedItem[]; +} + const relicQualitySuffixes: Record = { VPQ_BRONZE: "", VPQ_SILVER: " [Flawless]", @@ -41,24 +64,28 @@ const relicQualitySuffixes: Record = { const getItemListsController: RequestHandler = (req, response) => { const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); - const res: Record = {}; - res.Suits = []; - res.LongGuns = []; - res.Melee = []; - res.ModularParts = []; - res.Pistols = []; - res.Sentinels = []; - res.SentinelWeapons = []; - res.SpaceGuns = []; - res.SpaceMelee = []; - res.SpaceSuits = []; - res.MechSuits = []; - res.miscitems = []; - res.Syndicates = []; - res.OperatorAmps = []; - res.QuestKeys = []; - res.KubrowPets = []; - res.MoaPets = []; + const res: ItemLists = { + archonCrystalUpgrades, + uniqueLevelCaps: ExportMisc.uniqueLevelCaps, + Suits: [], + LongGuns: [], + Melee: [], + ModularParts: [], + Pistols: [], + Sentinels: [], + SentinelWeapons: [], + SpaceGuns: [], + SpaceMelee: [], + SpaceSuits: [], + MechSuits: [], + miscitems: [], + Syndicates: [], + OperatorAmps: [], + QuestKeys: [], + KubrowPets: [], + MoaPets: [], + mods: [] + }; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ uniqueName, @@ -201,7 +228,6 @@ const getItemListsController: RequestHandler = (req, response) => { }); } - res.mods = []; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { const mod: ListedItem = { uniqueName, @@ -260,11 +286,7 @@ const getItemListsController: RequestHandler = (req, response) => { } } - response.json({ - archonCrystalUpgrades, - uniqueLevelCaps: ExportMisc.uniqueLevelCaps, - ...res - }); + response.json(res); }; export { getItemListsController }; From efc7467a9962ff7eb63196c464b28f7156797de8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 02:00:27 +0200 Subject: [PATCH 590/776] chore: remove unused MoaPets array from getItemLists --- src/controllers/custom/getItemListsController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index a2c98c7d..38c304f7 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -51,7 +51,6 @@ interface ItemLists { OperatorAmps: ListedItem[]; QuestKeys: ListedItem[]; KubrowPets: ListedItem[]; - MoaPets: ListedItem[]; mods: ListedItem[]; } @@ -83,7 +82,6 @@ const getItemListsController: RequestHandler = (req, response) => { OperatorAmps: [], QuestKeys: [], KubrowPets: [], - MoaPets: [], mods: [] }; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { @@ -94,7 +92,7 @@ const getItemListsController: RequestHandler = (req, response) => { }); } for (const [uniqueName, item] of Object.entries(ExportSentinels)) { - if (item.productCategory != "SpecialItems") { + if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") { res[item.productCategory].push({ uniqueName, name: getString(item.name, lang), From 756a01d270cc1dfc0f3981eeb7508df8f1c22c15 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 05:36:08 +0200 Subject: [PATCH 591/776] fix: pass Emblem field on in getGuildClient --- src/services/guildService.ts | 1 + src/types/guildTypes.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index aba101e6..5aa7aa77 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -105,6 +105,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s Members: members, Ranks: guild.Ranks, Tier: guild.Tier, + Emblem: guild.Emblem, Vault: getGuildVault(guild), ActiveDojoColorResearch: guild.ActiveDojoColorResearch, Class: guild.Class, diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index dc71e3ec..c0e2cbe3 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -11,6 +11,7 @@ export interface IGuildClient { Members: IGuildMemberClient[]; Ranks: IGuildRank[]; Tier: number; + Emblem?: boolean; Vault: IGuildVault; ActiveDojoColorResearch: string; Class: number; From a67f99b665514ee337ecf42d3d5f5b644e8e7fc9 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:24:11 -0700 Subject: [PATCH 592/776] chore: don't use sequential values as RNG seeds directly (#1812) This should help get a slightly better distribution Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1812 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 34 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 18cc9d5c..f726e603 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -225,7 +225,8 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { return; } - const rng = new CRng(day); + const seed = new CRng(day).randomInt(0, 0xffff); + const rng = new CRng(seed); const boss = rng.randomElement(sortieBosses); @@ -354,7 +355,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", - Seed: day, + Seed: seed, Boss: boss, Variants: selectedNodes }); @@ -374,7 +375,7 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { const dayStart = EPOCH + day * 86400000; const dayEnd = EPOCH + (day + 3) * 86400000; - const rng = new CRng(day); + const rng = new CRng(new CRng(day).randomInt(0, 0xffff)); return { _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, Daily: true, @@ -394,7 +395,7 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; - const rng = new CRng(challengeId); + const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff)); return { _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, @@ -411,7 +412,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; - const rng = new CRng(challengeId); + const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff)); return { _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, @@ -476,7 +477,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => { //logger.debug(`birthday on day ${day}`); eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0 } - const rng = new CRng(week); + const rng = new CRng(new CRng(week).randomInt(0, 0xffff)); const challenges = [ "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy", "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium", @@ -710,7 +711,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Elite Sanctuary Onslaught cycling every week - worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful + worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new CRng(week).randomInt(0, 0xffff); // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation let bountyCycle = Math.trunc(Date.now() / 9000000); @@ -749,14 +750,16 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // TODO: xpAmounts need to be calculated based on the jobType somehow? + const seed = new CRng(bountyCycle).randomInt(0, 0xffff); + { - const rng = new CRng(bountyCycle); + const rng = new CRng(seed); worldState.SyndicateMissions.push({ _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000008" }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "CetusSyndicate", - Seed: bountyCycle, + Seed: seed, Nodes: [], Jobs: [ { @@ -820,13 +823,13 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } { - const rng = new CRng(bountyCycle); + const rng = new CRng(seed); worldState.SyndicateMissions.push({ _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000025" }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "SolarisSyndicate", - Seed: bountyCycle, + Seed: seed, Nodes: [], Jobs: [ { @@ -890,13 +893,13 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } { - const rng = new CRng(bountyCycle); + const rng = new CRng(seed); worldState.SyndicateMissions.push({ _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000002" }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "EntratiSyndicate", - Seed: bountyCycle, + Seed: seed, Nodes: [], Jobs: [ { @@ -1119,7 +1122,8 @@ export const getLiteSortie = (week: number): ILiteSortie => { } } - const rng = new CRng(week); + const seed = new CRng(week).randomInt(0, 0xffff); + const rng = new CRng(seed); const firstNodeIndex = rng.randomInt(0, nodes.length - 1); const firstNode = nodes[firstNodeIndex]; nodes.splice(firstNodeIndex, 1); @@ -1133,7 +1137,7 @@ export const getLiteSortie = (week: number): ILiteSortie => { Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", - Seed: week, + Seed: seed, Boss: boss, Missions: [ { From 8c32dc2670f7355f18c2c842765d0f6ddf727493 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:24:25 -0700 Subject: [PATCH 593/776] fix: add MoaPets into sellController (#1813) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1813 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/sellController.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index 4a2025c1..8e9d7e08 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -45,7 +45,7 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) { requiredFields.add(InventorySlot.SPACEWEAPONS); } - if (payload.Items.Sentinels || payload.Items.SentinelWeapons) { + if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) { requiredFields.add(InventorySlot.SENTINELS); } if (payload.Items.OperatorAmps) { @@ -148,6 +148,12 @@ export const sellController: RequestHandler = async (req, res) => { freeUpSlot(inventory, InventorySlot.SENTINELS); }); } + if (payload.Items.MoaPets) { + payload.Items.MoaPets.forEach(sellItem => { + inventory.MoaPets.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SENTINELS); + }); + } if (payload.Items.OperatorAmps) { payload.Items.OperatorAmps.forEach(sellItem => { inventory.OperatorAmps.pull({ _id: sellItem.String }); @@ -281,6 +287,7 @@ interface ISellRequest { SpaceMelee?: ISellItem[]; Sentinels?: ISellItem[]; SentinelWeapons?: ISellItem[]; + MoaPets?: ISellItem[]; OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; Drones?: ISellItem[]; From 409c089d11ff77d4b07a07c48258f6c4afd7efc3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:24:38 -0700 Subject: [PATCH 594/776] feat: handle account already owning a nightwave skin item (#1814) Closes #1811 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1814 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/syndicateSacrificeController.ts | 15 +++++++++++---- src/services/inventoryService.ts | 16 ++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index fd385999..2dc1d67d 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -6,6 +6,9 @@ import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { isStoreItem, toStoreItem } from "@/src/services/itemDataService"; +import { logger } from "@/src/utils/logger"; + +const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName; export const syndicateSacrificeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -74,10 +77,14 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp if (!isStoreItem(rewardType)) { rewardType = toStoreItem(rewardType); } - combineInventoryChanges( - res.InventoryChanges, - (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges - ); + const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)) + .InventoryChanges; + if (Object.keys(rewardInventoryChanges).length == 0) { + logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); + rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }]; + addMiscItems(inventory, rewardInventoryChanges.MiscItems); + } + combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); } } diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 84984dc9..a9176e0b 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1236,12 +1236,16 @@ export const addSkin = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - inventoryChanges.WeaponSkins ??= []; - (inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push( - inventory.WeaponSkins[index].toJSON() - ); + if (inventory.WeaponSkins.find(x => x.ItemType == typeName)) { + logger.debug(`refusing to add WeaponSkin ${typeName} because account already owns it`); + } else { + const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.WeaponSkins ??= []; + (inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push( + inventory.WeaponSkins[index].toJSON() + ); + } return inventoryChanges; }; From 100aefcee4f7b7c2c05f1b07834c3c390ae9c456 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:24:53 -0700 Subject: [PATCH 595/776] fix: give corresponding weapon when crafting Hound (#1816) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1816 Reviewed-by: Sainan Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../api/modularWeaponCraftingController.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 0d083371..91809b49 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -17,7 +17,7 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { getRandomInt } from "@/src/services/rngService"; -import { ExportSentinels, IDefaultUpgrade } from "warframe-public-export-plus"; +import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus"; import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { @@ -138,6 +138,18 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) } else { defaultUpgrades = getDefaultUpgrades(data.Parts); } + + if (category == "MoaPets") { + const weapon = ExportSentinels[data.WeaponType].defaultWeapon; + if (weapon) { + const category = ExportWeapons[weapon].productCategory; + addEquipment(inventory, category, weapon, undefined, inventoryChanges); + combineInventoryChanges( + inventoryChanges, + occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi) + ); + } + } defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); combineInventoryChanges( From 826a09a47375af2d78616bdac847e286b2af73da Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:51:43 -0700 Subject: [PATCH 596/776] fix: provide a response to setShipFavouriteLoadout (#1824) Seems to be the same format as the request, so just mirror it back. This is so the client knows we acknowledged the change as it won't resync the ship until the next login. Closes #1822 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1824 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/setShipFavouriteLoadoutController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/setShipFavouriteLoadoutController.ts b/src/controllers/api/setShipFavouriteLoadoutController.ts index e4bf2e13..a7df934f 100644 --- a/src/controllers/api/setShipFavouriteLoadoutController.ts +++ b/src/controllers/api/setShipFavouriteLoadoutController.ts @@ -20,7 +20,7 @@ export const setShipFavouriteLoadoutController: RequestHandler = async (req, res throw new Error(`unexpected BootLocation: ${body.BootLocation}`); } await personalRooms.save(); - res.json({}); + res.json(body); }; interface ISetShipFavouriteLoadoutRequest { From 3ffa4a7fd323be379fa0041de7d998979beed398 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:51:54 -0700 Subject: [PATCH 597/776] fix: exclude some more nodes from syndicate missions (#1825) Closes #1819 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1825 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index f726e603..9178e48c 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -186,10 +186,12 @@ const pushSyndicateMissions = ( for (const [key, value] of Object.entries(ExportRegions)) { if ( !isArchwingMission(value) && - value.systemIndex != 23 && // no 1999 stuff + !value.questReq && // Exclude zariman, murmor, and 1999 stuff + !value.hidden && // Exclude the index + !value.darkSectorData && // Exclude dark sectors value.missionIndex != 10 && // Exclude MT_PVP (for relays) value.missionIndex != 23 && // no junctions - value.missionIndex <= 28 // no railjack or some such + value.missionIndex < 28 // no open worlds, railjack, etc ) { nodeOptions.push(key); } From 70646160c394a4034494f3825bf5506da6ff7a89 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:52:16 -0700 Subject: [PATCH 598/776] fix: give no rewards if there are no qualifications (#1826) Fixes #1823 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1826 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 05b94b0a..27f31e5a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -73,13 +73,11 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] return [rewardInfo.rewardTier]; } - // Aborting a railjack mission should not give any rewards (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) - if (rewardInfo.rewardQualifications === undefined) { - return []; - } + const rotationCount = rewardInfo.rewardQualifications?.length || 0; - const rotationCount = rewardInfo.rewardQualifications.length || 0; - if (rotationCount === 0) return [0]; + // Empty or absent rewardQualifications should not give rewards: + // - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) + // - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823) const rotationPattern = tierOverride === undefined @@ -792,7 +790,8 @@ export const addMissionRewards = async ( missions.Tag != "SolNode761" && // the index missions.Tag != "SolNode762" && // the index missions.Tag != "SolNode763" && // the index - missions.Tag != "CrewBattleNode556" // free flight + missions.Tag != "CrewBattleNode556" && // free flight + getRotations(rewardInfo).length > 0 // (E)SO should not give credits for only completing zone 1, in which case it has no rewardQualifications (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823) ) { const levelCreditReward = getLevelCreditRewards(node); missionCompletionCredits += levelCreditReward; From 506365f97e20d4c68acb364ce76b7dd01b4851de Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:52:31 -0700 Subject: [PATCH 599/776] feat: auto-generate debt token vendor manifest (#1827) Yet another pretty big change to how these things are generated, but getting closer to where we wanna be now. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1827 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/rngService.ts | 14 +- src/services/serversideVendorsService.ts | 163 ++++++++---- src/types/vendorTypes.ts | 1 + .../SolarisDebtTokenVendorManifest.json | 248 ------------------ 4 files changed, 131 insertions(+), 295 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json diff --git a/src/services/rngService.ts b/src/services/rngService.ts index f88b23c0..bb9028b2 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -97,9 +97,17 @@ export class CRng { } randomInt(min: number, max: number): number { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(this.random() * (max - min + 1)) + min; + const diff = max - min; + if (diff != 0) { + if (diff < 0) { + throw new Error(`max must be greater than min`); + } + if (diff > 0x3fffffff) { + throw new Error(`insufficient entropy`); + } + min += Math.floor(this.random() * (diff + 1)); + } + return min; } randomElement(arr: T[]): T { diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 3abe56fa..f13ca7e5 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -5,9 +5,10 @@ import { IItemManifestPreprocessed, IRawVendorManifest, IVendorInfo, + IVendorInfoPreprocessed, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; -import { ExportVendors } from "warframe-public-export-plus"; +import { ExportVendors, IRange } from "warframe-public-export-plus"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; @@ -32,7 +33,6 @@ import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorIn import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; -import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json"; import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; @@ -63,7 +63,6 @@ const rawVendorManifests: IRawVendorManifest[] = [ OstronPetVendorManifest, OstronProspectorVendorManifest, RadioLegionIntermission12VendorManifest, - SolarisDebtTokenVendorManifest, SolarisDebtTokenVendorRepossessionsManifest, SolarisFishmongerVendorManifest, SolarisProspectorVendorManifest, @@ -72,7 +71,7 @@ const rawVendorManifests: IRawVendorManifest[] = [ ]; interface IGeneratableVendorInfo extends Omit { - cycleStart: number; + cycleOffset: number; cycleDuration: number; } @@ -84,7 +83,7 @@ const generatableVendors: IGeneratableVendorInfo[] = [ RandomSeedType: "VRST_WEAPON", RequiredGoalTag: "", WeaponUpgradeValueAttenuationExponent: 2.25, - cycleStart: 1740960000_000, + cycleOffset: 1740960000_000, cycleDuration: 4 * unixTimesInMs.day }, { @@ -93,8 +92,16 @@ const generatableVendors: IGeneratableVendorInfo[] = [ PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70", RandomSeedType: "VRST_WEAPON", WeaponUpgradeValueAttenuationExponent: 2.25, - cycleStart: 1744934400_000, + cycleOffset: 1744934400_000, cycleDuration: 4 * unixTimesInMs.day + }, + { + _id: { $oid: "5be4a159b144f3cdf1c22efa" }, + TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", + PropertyTextHash: "A39621049CA3CA13761028CD21C239EF", + RandomSeedType: "VRST_FLAVOUR_TEXT", + cycleOffset: 1734307200_000, + cycleDuration: unixTimesInMs.hour } // { // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, @@ -166,60 +173,128 @@ const refreshExpiry = (expiry: IMongoDate): number => { return 0; }; -const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { - const EPOCH = vendorInfo.cycleStart; - const manifest = ExportVendors[vendorInfo.TypeName]; - let binThisCycle; - if (manifest.isOneBinPerCycle) { - const cycleDuration = vendorInfo.cycleDuration; // manifest.items[0].durationHours! * 3600_000; - const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); - binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. +const toRange = (value: IRange | number): IRange => { + if (typeof value == "number") { + return { minValue: value, maxValue: value }; } - const items: IItemManifestPreprocessed[] = []; - let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; - for (let i = 0; i != manifest.items.length; ++i) { - const rawItem = manifest.items[i]; - if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) { - continue; + return value; +}; + +const vendorInfoCache: Record = {}; + +const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { + if (!(vendorInfo.TypeName in vendorInfoCache)) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; + vendorInfoCache[vendorInfo.TypeName] = { + ...clientVendorInfo, + ItemManifest: [], + Expiry: { $date: { $numberLong: "0" } } + }; + } + const processed = vendorInfoCache[vendorInfo.TypeName]; + if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) { + // Remove expired offers + for (let i = 0; i != processed.ItemManifest.length; ) { + if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) { + processed.ItemManifest.splice(i, 1); + } else { + ++i; + } } - const cycleDuration = vendorInfo.cycleDuration; // rawItem.durationHours! * 3600_000; - const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); - const cycleStart = EPOCH + cycleIndex * cycleDuration; - const cycleEnd = cycleStart + cycleDuration; - if (soonestOfferExpiry > cycleEnd) { - soonestOfferExpiry = cycleEnd; + + // Add new offers + const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16); + const cycleIndex = Math.trunc((Date.now() - vendorInfo.cycleOffset) / vendorInfo.cycleDuration); + const rng = new CRng(mixSeeds(vendorSeed, cycleIndex)); + const manifest = ExportVendors[vendorInfo.TypeName]; + const offersToAdd = []; + if (manifest.numItems && manifest.numItems.minValue != manifest.numItems.maxValue) { + const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); + while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) { + // TODO: Consider per-bin item limits + // TODO: Consider item probability weightings + offersToAdd.push(rng.randomElement(manifest.items)); + } + } else { + let binThisCycle; + if (manifest.isOneBinPerCycle) { + binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. + } + for (const rawItem of manifest.items) { + if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) { + offersToAdd.push(rawItem); + } + } } - const rng = new CRng(cycleIndex); - rng.churnSeed(i); - /*for (let j = -1; j != rawItem.duplicates; ++j)*/ { + const cycleStart = vendorInfo.cycleOffset + cycleIndex * vendorInfo.cycleDuration; + for (const rawItem of offersToAdd) { + const durationHoursRange = toRange(rawItem.durationHours); + const expiry = + cycleStart + + rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; const item: IItemManifestPreprocessed = { StoreItem: rawItem.storeItem, - ItemPrices: rawItem.itemPrices!.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), + ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), Bin: "BIN_" + rawItem.bin, QuantityMultiplier: 1, - Expiry: { $date: { $numberLong: cycleEnd.toString() } }, + Expiry: { $date: { $numberLong: expiry.toString() } }, AllowMultipurchase: false, Id: { $oid: - i.toString(16).padStart(8, "0") + + Math.trunc(cycleStart / 1000) + .toString(16) + .padStart(8, "0") + vendorInfo._id.$oid.substring(8, 16) + - rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") + rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") + + rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") } }; - if (vendorInfo.RandomSeedType) { - item.LocTagRandSeed = - (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)); + if (rawItem.numRandomItemPrices) { + item.ItemPrices = []; + for (let i = 0; i != rawItem.numRandomItemPrices; ++i) { + let itemPrice: { type: string; count: IRange }; + do { + itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin]); + } while (item.ItemPrices.find(x => x.ItemType == itemPrice.type)); + item.ItemPrices.push({ + ItemType: itemPrice.type, + ItemCount: rng.randomInt(itemPrice.count.minValue, itemPrice.count.maxValue), + ProductCategory: "MiscItems" + }); + } } - items.push(item); + if (rawItem.credits) { + const value = + typeof rawItem.credits == "number" + ? rawItem.credits + : rng.randomInt( + rawItem.credits.minValue / rawItem.credits.step, + rawItem.credits.maxValue / rawItem.credits.step + ) * rawItem.credits.step; + item.RegularPrice = [value, value]; + } + if (vendorInfo.RandomSeedType) { + item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); + if (vendorInfo.RandomSeedType == "VRST_WEAPON") { + const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); + item.LocTagRandSeed = (BigInt(highDword) << 32n) | BigInt(item.LocTagRandSeed); + } + } + processed.ItemManifest.push(item); } + + // Update vendor expiry + let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; + for (const offer of processed.ItemManifest) { + const offerExpiry = parseInt(offer.Expiry.$date.$numberLong); + if (soonestOfferExpiry > offerExpiry) { + soonestOfferExpiry = offerExpiry; + } + } + processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { cycleStart, cycleDuration, ...clientVendorInfo } = vendorInfo; return { - VendorInfo: { - ...clientVendorInfo, - ItemManifest: items, - Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } } - } + VendorInfo: processed }; }; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index f14d3f55..a6e01835 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -13,6 +13,7 @@ export interface IItemPricePreprocessed extends Omit { export interface IItemManifest { StoreItem: string; ItemPrices?: IItemPrice[]; + RegularPrice?: number[]; Bin: string; QuantityMultiplier: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. diff --git a/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json b/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json deleted file mode 100644 index 3a4fa0ac..00000000 --- a/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5be4a159b144f3cdf1c22efa" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Gameplay/Venus/Resources/VenusCoconutItem", - "ItemCount": 5, - "ProductCategory": "MiscItems" - }, - { - "ItemType": "/Lotus/Types/Items/MiscItems/Circuits", - "ItemCount": 3664, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [87300, 87300], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1881404827, - "Id": { - "$oid": "670daf92d21f34757a5e73b4" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleRareC", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/NeuralSensor", - "ItemCount": 1, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [53300, 53300], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1943984533, - "Id": { - "$oid": "6710b5029e1a3080a65e73a7" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonG", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/Salvage", - "ItemCount": 11540, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [27300, 27300], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 744199559, - "Id": { - "$oid": "67112582cc115756985e73a4" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem", - "ItemCount": 9, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [75800, 75800], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 3744711432, - "Id": { - "$oid": "670de7d28a6ec82cd25e73a2" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/Rubedo", - "ItemCount": 3343, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [52200, 52200], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1579000687, - "Id": { - "$oid": "670e58526171148e125e73ad" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonA", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Gameplay/Venus/Resources/CoolantItem", - "ItemCount": 9, - "ProductCategory": "MiscItems" - }, - { - "ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem", - "ItemCount": 5, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [12400, 12400], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 3589081466, - "Id": { - "$oid": "67112582cc115756985e73a5" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonC", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonOreBAlloyItem", - "ItemCount": 13, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [77500, 77500], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1510234814, - "Id": { - "$oid": "670f0f21250ad046c35e73ee" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishParralelBiodeItem", - "ItemCount": 7, - "ProductCategory": "MiscItems" - }, - { - "ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonGemBCutItem", - "ItemCount": 12, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [94600, 94600], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 4222095721, - "Id": { - "$oid": "670f63827be40254f95e739d" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonJ", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/Nanospores", - "ItemCount": 14830, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [25600, 25600], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 2694388669, - "Id": { - "$oid": "67112582cc115756985e73a6" - } - } - ], - "PropertyTextHash": "A39621049CA3CA13761028CD21C239EF", - "RandomSeedType": "VRST_FLAVOUR_TEXT", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} From 6b3f5245748e1e11e692c3dee79f76ee4a134ab0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:52:42 -0700 Subject: [PATCH 600/776] feat: sortie mission credit rewards (#1828) Closes #1820 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1828 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 8 ++++++++ src/types/requestTypes.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 27f31e5a..625a7fd0 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -803,6 +803,14 @@ export const addMissionRewards = async ( missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); } + if (rewardInfo.sortieTag == "Mission1") { + missionCompletionCredits += 20_000; + } else if (rewardInfo.sortieTag == "Mission2") { + missionCompletionCredits += 30_000; + } else if (rewardInfo.sortieTag == "Final") { + missionCompletionCredits += 50_000; + } + if (missions.Tag == "PlutoToErisJunction") { await createMessage(inventory.accountOwnerId, [ { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 441bf03e..74764bc1 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -135,7 +135,7 @@ export interface IRewardInfo { invasionId?: string; invasionAllyFaction?: "FC_GRINEER" | "FC_CORPUS"; sortieId?: string; - sortieTag?: string; + sortieTag?: "Mission1" | "Mission2" | "Final"; sortiePrereqs?: string[]; VaultsCracked?: number; // for Spy missions rewardTier?: number; From fa6fac494b1794665cd4484d43c358b24ca26660 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:53:04 -0700 Subject: [PATCH 601/776] fix: some problems with 1999 calendar rotation (#1829) - First day was incorrect for summer & autumn - Only 1 reward was shown, now is a choice of 2 - Only 1 upgrade was shown, now is a choice of 3 - First 2 challenges in the season are now guaranteed to be "easy" Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1829 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 142 +++++++++++++++++++----------- 1 file changed, 89 insertions(+), 53 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 9178e48c..e1af4369 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -7,6 +7,7 @@ import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, + ICalendarEvent, ICalendarSeason, ILiteSortie, ISeasonChallenge, @@ -466,8 +467,8 @@ const birthdays: number[] = [ const getCalendarSeason = (week: number): ICalendarSeason => { const seasonIndex = week % 4; - const seasonDay1 = seasonIndex * 90 + 1; - const seasonDay91 = seasonIndex * 90 + 91; + const seasonDay1 = [1, 91, 182, 274][seasonIndex]; + const seasonDay91 = seasonDay1 + 90; const eventDays: ICalendarDay[] = []; for (const day of birthdays) { if (day < seasonDay1) { @@ -526,8 +527,12 @@ const getCalendarSeason = (week: number): ICalendarSeason => { challengeDay = rng.randomInt(chunkDay1, chunkDay13); } while (birthdays.indexOf(challengeDay) != -1); - const challengeIndex = rng.randomInt(0, challenges.length - 1); - const challenge = challenges[challengeIndex]; + let challengeIndex; + let challenge; + do { + challengeIndex = rng.randomInt(0, challenges.length - 1); + challenge = challenges[challengeIndex]; + } while (i < 2 && !challenge.endsWith("Easy")); // First 2 challenges should be easy challenges.splice(challengeIndex, 1); //logger.debug(`challenge on day ${challengeDay}`); @@ -574,69 +579,100 @@ const getCalendarSeason = (week: number): ICalendarSeason => { "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalViolet" ]; for (let i = 0; i != rewardRanges.length - 1; ++i) { - const rewardIndex = rng.randomInt(0, rewards.length - 1); - const reward = rewards[rewardIndex]; - rewards.splice(rewardIndex, 1); + const events: ICalendarEvent[] = []; + for (let j = 0; j != 2; ++j) { + const rewardIndex = rng.randomInt(0, rewards.length - 1); + events.push({ type: "CET_REWARD", reward: rewards[rewardIndex] }); + rewards.splice(rewardIndex, 1); + } - //logger.debug(`trying to fit a reward between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`); + //logger.debug(`trying to fit rewards between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`); let day: number; do { day = rng.randomInt(rewardRanges[i] + 1, rewardRanges[i + 1] - 1); } while (eventDays.find(x => x.day == day)); - eventDays.push({ day, events: [{ type: "CET_REWARD", reward }] }); + eventDays.push({ day, events }); } - const upgrades = [ - "/Lotus/Upgrades/Calendar/MeleeCritChance", - "/Lotus/Upgrades/Calendar/MeleeAttackSpeed", - "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange", - "/Lotus/Upgrades/Calendar/AbilityStrength", - "/Lotus/Upgrades/Calendar/Armor", - "/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage", - "/Lotus/Upgrades/Calendar/CompanionDamage", - "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary", - "/Lotus/Upgrades/Calendar/MagazineCapacity", - "/Lotus/Upgrades/Calendar/PunchToPrimary", - "/Lotus/Upgrades/Calendar/HealingEffects", - "/Lotus/Upgrades/Calendar/EnergyRestoration", - "/Lotus/Upgrades/Calendar/OvershieldCap", - "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance", - "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier", - "/Lotus/Upgrades/Calendar/MagnetStatusPull", - "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer", - "/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent", - "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup", - "/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee", - "/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy", - "/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts", - "/Lotus/Upgrades/Calendar/CompanionsRadiationChance", - "/Lotus/Upgrades/Calendar/BlastEveryXShots", - "/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill", - "/Lotus/Upgrades/Calendar/ElectricDamagePerDistance", - "/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit", - "/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts", - "/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill", - "/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc", - "/Lotus/Upgrades/Calendar/ExplodingHealthOrbs", - "/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne", - "/Lotus/Upgrades/Calendar/EnergyWavesOnCombo", - "/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent", - "/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent", - "/Lotus/Upgrades/Calendar/GuidingMissilesChance", - "/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield", - "/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump" + const upgradesByHexMember = [ + [ + "/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee", + "/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump", + "/Lotus/Upgrades/Calendar/ElectricDamagePerDistance", + "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance", + "/Lotus/Upgrades/Calendar/OvershieldCap", + "/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne" + ], + [ + "/Lotus/Upgrades/Calendar/AbilityStrength", + "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange", + "/Lotus/Upgrades/Calendar/MagnetStatusPull", + "/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts", + "/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent", + "/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts" + ], + [ + "/Lotus/Upgrades/Calendar/EnergyWavesOnCombo", + "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier", + "/Lotus/Upgrades/Calendar/MeleeAttackSpeed", + "/Lotus/Upgrades/Calendar/MeleeCritChance", + "/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit", + "/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy" + ], + [ + "/Lotus/Upgrades/Calendar/Armor", + "/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent", + "/Lotus/Upgrades/Calendar/CompanionDamage", + "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer", + "/Lotus/Upgrades/Calendar/CompanionsRadiationChance", + "/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage", + "/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill" + ], + [ + "/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield", + "/Lotus/Upgrades/Calendar/EnergyRestoration", + "/Lotus/Upgrades/Calendar/ExplodingHealthOrbs", + "/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill", + "/Lotus/Upgrades/Calendar/HealingEffects", + "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup" + ], + [ + "/Lotus/Upgrades/Calendar/BlastEveryXShots", + "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary", + "/Lotus/Upgrades/Calendar/GuidingMissilesChance", + "/Lotus/Upgrades/Calendar/MagazineCapacity", + "/Lotus/Upgrades/Calendar/PunchToPrimary", + "/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc", + "/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent" + ] ]; for (let i = 0; i != upgradeRanges.length - 1; ++i) { - const upgradeIndex = rng.randomInt(0, upgrades.length - 1); - const upgrade = upgrades[upgradeIndex]; - upgrades.splice(upgradeIndex, 1); + // Pick 3 unique hex members + const hexMembersPickedForThisDay: number[] = []; + for (let j = 0; j != 3; ++j) { + let hexMemberIndex: number; + do { + hexMemberIndex = rng.randomInt(0, upgradesByHexMember.length - 1); + } while (hexMembersPickedForThisDay.indexOf(hexMemberIndex) != -1); + hexMembersPickedForThisDay.push(hexMemberIndex); + } + hexMembersPickedForThisDay.sort(); // Always present them in the same order - //logger.debug(`trying to fit an upgrade between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`); + // For each hex member, pick an upgrade that was not yet picked this season. + const events: ICalendarEvent[] = []; + for (const hexMemberIndex of hexMembersPickedForThisDay) { + const upgrades = upgradesByHexMember[hexMemberIndex]; + const upgradeIndex = rng.randomInt(0, upgrades.length - 1); + events.push({ type: "CET_UPGRADE", upgrade: upgrades[upgradeIndex] }); + upgrades.splice(upgradeIndex, 1); + } + + //logger.debug(`trying to fit upgrades between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`); let day: number; do { day = rng.randomInt(upgradeRanges[i] + 1, upgradeRanges[i + 1] - 1); } while (eventDays.find(x => x.day == day)); - eventDays.push({ day, events: [{ type: "CET_UPGRADE", upgrade }] }); + eventDays.push({ day, events }); } eventDays.sort((a, b) => a.day - b.day); From fd7f4c9e92c6a421453fd60e08588ccd8bf07b75 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:53:34 -0700 Subject: [PATCH 602/776] feat: calendar progress (#1830) Closes #1775 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1830 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/completeCalendarEventController.ts | 41 ++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 12 ++--- src/routes/api.ts | 2 + src/services/inventoryService.ts | 54 +++++++++++++------ src/services/missionInventoryUpdateService.ts | 10 ++++ src/services/worldStateService.ts | 2 +- src/types/inventoryTypes/inventoryTypes.ts | 9 ++-- src/types/requestTypes.ts | 1 + src/types/worldStateTypes.ts | 2 +- 9 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 src/controllers/api/completeCalendarEventController.ts diff --git a/src/controllers/api/completeCalendarEventController.ts b/src/controllers/api/completeCalendarEventController.ts new file mode 100644 index 00000000..20c8abb3 --- /dev/null +++ b/src/controllers/api/completeCalendarEventController.ts @@ -0,0 +1,41 @@ +import { getCalendarProgress, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { getWorldState } from "@/src/services/worldStateService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER +export const completeCalendarEventController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const calendarProgress = getCalendarProgress(inventory); + const currentSeason = getWorldState().KnownCalendarSeasons[0]; + let inventoryChanges: IInventoryChanges = {}; + let dayIndex = 0; + for (const day of currentSeason.Days) { + if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") { + if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) { + if (day.events.length != 0) { + const selection = day.events[parseInt(req.query.CompletedEventIdx as string)]; + if (selection.type == "CET_REWARD") { + inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)) + .InventoryChanges; + } else if (selection.type == "CET_UPGRADE") { + calendarProgress.YearProgress.Upgrades.push(selection.upgrade!); + } else if (selection.type != "CET_PLOT") { + throw new Error(`unexpected selection type: ${selection.type}`); + } + } + break; + } + ++dayIndex; + } + } + calendarProgress.SeasonProgress.LastCompletedDayIdx++; + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + CalendarProgress: inventory.CalendarProgress + }); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 6b73186f..2e09caf8 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1125,15 +1125,15 @@ const CustomMarkersSchema = new Schema( const calenderProgressSchema = new Schema( { Version: { type: Number, default: 19 }, - Iteration: { type: Number, default: 2 }, + Iteration: { type: Number, required: true }, YearProgress: { - Upgrades: { type: [] } + Upgrades: { type: [String], default: [] } }, SeasonProgress: { - SeasonType: String, - LastCompletedDayIdx: { type: Number, default: -1 }, - LastCompletedChallengeDayIdx: { type: Number, default: -1 }, - ActivatedChallenges: [] + SeasonType: { type: String, required: true }, + LastCompletedDayIdx: { type: Number, default: 0 }, + LastCompletedChallengeDayIdx: { type: Number, default: 0 }, + ActivatedChallenges: { type: [String], default: [] } } }, { _id: false } diff --git a/src/routes/api.ts b/src/routes/api.ts index c3dc2e50..b11af3e4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -19,6 +19,7 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; +import { completeCalendarEventController } from "@/src/controllers/api/completeCalendarEventController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController"; import { confirmGuildInvitationGetController, confirmGuildInvitationPostController } from "@/src/controllers/api/confirmGuildInvitationController"; @@ -158,6 +159,7 @@ apiRouter.get("/changeDojoRoot.php", changeDojoRootController); apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); +apiRouter.get("/completeCalendarEvent.php", completeCalendarEventController); apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController); apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationGetController); apiRouter.get("/credits.php", creditsController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index a9176e0b..94554299 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -18,7 +18,6 @@ import { IKubrowPetEggDatabase, IKubrowPetEggClient, ILibraryDailyTaskInfo, - ICalendarProgress, IDroneClient, IUpgradeClient, TPartialStartingGear, @@ -26,7 +25,8 @@ import { ICrewMemberClient, Status, IKubrowPetDetailsDatabase, - ITraits + ITraits, + ICalendarProgress } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -78,6 +78,7 @@ import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService"; import { createMessage } from "./inboxService"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; +import { getWorldState } from "./worldStateService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -91,7 +92,6 @@ export const createInventory = async ( }); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); - inventory.CalendarProgress = createCalendar(); inventory.RewardSeed = generateRewardSeed(); inventory.DuviriInfo = { Seed: generateRewardSeed(), @@ -1756,20 +1756,6 @@ export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => { }; }; -const createCalendar = (): ICalendarProgress => { - return { - Version: 19, - Iteration: 2, - YearProgress: { Upgrades: [] }, - SeasonProgress: { - SeasonType: "CST_SPRING", - LastCompletedDayIdx: -1, - LastCompletedChallengeDayIdx: -1, - ActivatedChallenges: [] - } - }; -}; - export const setupKahlSyndicate = (inventory: TInventoryDatabaseDocument): void => { inventory.Affiliations.push({ Title: 1, @@ -1806,3 +1792,37 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => LibrarySyndicate.FreeFavorsEarned = undefined; } }; + +export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { + const currentSeason = getWorldState().KnownCalendarSeasons[0]; + + if (!inventory.CalendarProgress) { + inventory.CalendarProgress = { + Version: 19, + Iteration: currentSeason.YearIteration, + YearProgress: { + Upgrades: [] + }, + SeasonProgress: { + SeasonType: currentSeason.Season, + LastCompletedDayIdx: 0, + LastCompletedChallengeDayIdx: 0, + ActivatedChallenges: [] + } + }; + } + + const yearRolledOver = inventory.CalendarProgress.Iteration != currentSeason.YearIteration; + if (yearRolledOver) { + inventory.CalendarProgress.Iteration = currentSeason.YearIteration; + inventory.CalendarProgress.YearProgress.Upgrades = []; + } + if (yearRolledOver || inventory.CalendarProgress.SeasonProgress.SeasonType != currentSeason.Season) { + inventory.CalendarProgress.SeasonProgress.SeasonType = currentSeason.Season; + inventory.CalendarProgress.SeasonProgress.LastCompletedDayIdx = -1; + inventory.CalendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = -1; + inventory.CalendarProgress.SeasonProgress.ActivatedChallenges = []; + } + + return inventory.CalendarProgress; +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 625a7fd0..cb55fc69 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -33,6 +33,7 @@ import { addStanding, combineInventoryChanges, generateRewardSeed, + getCalendarProgress, updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; @@ -560,6 +561,15 @@ export const addMissionInventoryUpdates = async ( } break; } + case "CalendarProgress": { + const calendarProgress = getCalendarProgress(inventory); + for (const progress of value) { + const challengeName = progress.challenge.substring(progress.challenge.lastIndexOf("/") + 1); + calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx++; + calendarProgress.SeasonProgress.ActivatedChallenges.push(challengeName); + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index e1af4369..4619d27a 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -683,7 +683,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => { Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, Days: eventDays, - Season: ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][seasonIndex], + Season: (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[seasonIndex], YearIteration: Math.trunc(week / 4), Version: 19, UpgradeAvaliabilityRequirements: ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"] diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 8ddfbf42..66ce5b3c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -353,7 +353,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu DeathSquadable: boolean; EndlessXP?: IEndlessXpProgress[]; DialogueHistory?: IDialogueHistoryClient; - CalendarProgress: ICalendarProgress; + CalendarProgress?: ICalendarProgress; SongChallenges?: ISongChallenge[]; EntratiVaultCountLastPeriod?: number; EntratiVaultCountResetDate?: IMongoDate; @@ -1193,17 +1193,18 @@ export interface IMarker { z: number; showInHud: boolean; } + export interface ISeasonProgress { - SeasonType: "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"; + SeasonType: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"; LastCompletedDayIdx: number; LastCompletedChallengeDayIdx: number; - ActivatedChallenges: unknown[]; + ActivatedChallenges: string[]; } export interface ICalendarProgress { Version: number; Iteration: number; - YearProgress: { Upgrades: unknown[] }; + YearProgress: { Upgrades: string[] }; SeasonProgress: ISeasonProgress; } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 74764bc1..d5b04058 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -44,6 +44,7 @@ export type IMissionInventoryUpdateRequest = { SyndicateId?: string; SortieId?: string; + CalendarProgress?: { challenge: string }[]; SeasonChallengeCompletions?: ISeasonChallenge[]; AffiliationChanges?: IAffiliationChange[]; crossPlaySetting?: string; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 835792b8..303c3c33 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -133,7 +133,7 @@ export interface ISeasonChallenge { export interface ICalendarSeason { Activation: IMongoDate; Expiry: IMongoDate; - Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL" + Season: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"; Days: ICalendarDay[]; YearIteration: number; Version: number; From 0b757572776b0b9bcd98523371d4281d589fdcfe Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:53:54 -0700 Subject: [PATCH 603/776] chore: improve distribution of rewardSeed (#1831) This was previously not ideal due to float imprecision, but now it's 64 bits and there's enough entropy for all of them. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1831 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 2 +- src/services/inventoryService.ts | 13 +++++++++---- src/types/inventoryTypes/inventoryTypes.ts | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 2e09caf8..c9dd4be8 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -390,7 +390,7 @@ MailboxSchema.set("toJSON", { const DuviriInfoSchema = new Schema( { - Seed: Number, + Seed: BigInt, NumCompletions: { type: Number, default: 0 } }, { diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 94554299..9d1bf0ed 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -120,10 +120,15 @@ export const createInventory = async ( } }; -export const generateRewardSeed = (): number => { - const min = -Number.MAX_SAFE_INTEGER; - const max = Number.MAX_SAFE_INTEGER; - return Math.floor(Math.random() * (max - min + 1)) + min; +export const generateRewardSeed = (): bigint => { + const hiDword = getRandomInt(0, 0x7fffffff); + const loDword = getRandomInt(0, 0xffffffff); + let seed = (BigInt(hiDword) << 32n) | BigInt(loDword); + if (Math.random() < 0.5) { + seed *= -1n; + seed -= 1n; + } + return seed; }; //TODO: RawUpgrades might need to return a LastAdded diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 66ce5b3c..b7c63a75 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -134,7 +134,7 @@ export const equipmentKeys = [ export type TEquipmentKey = (typeof equipmentKeys)[number]; export interface IDuviriInfo { - Seed: number; + Seed: bigint; NumCompletions: number; } @@ -202,7 +202,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Mailbox?: IMailboxClient; SubscribedToEmails: number; Created: IMongoDate; - RewardSeed: number | bigint; + RewardSeed: bigint; RegularCredits: number; PremiumCredits: number; PremiumCreditsFree: number; From 143b358a03a9b1f051748b1f02de137a0253ccb4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:54:11 -0700 Subject: [PATCH 604/776] chore: always update rewardSeed in missionInventoryUpdate (#1833) This should be slightly more faithful. Also logging a warning in case we have a mismatch as that shouldn't happen. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1833 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/missionInventoryUpdateController.ts | 4 +++- src/services/missionInventoryUpdateService.ts | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index bde1c3cf..ff181e0b 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -3,7 +3,7 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService"; -import { getInventory } from "@/src/services/inventoryService"; +import { generateRewardSeed, getInventory } from "@/src/services/inventoryService"; import { getInventoryResponse } from "./inventoryController"; import { logger } from "@/src/utils/logger"; import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes"; @@ -63,6 +63,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) missionReport.MissionStatus !== "GS_SUCCESS" && !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId) ) { + inventory.RewardSeed = generateRewardSeed(); await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); res.json({ @@ -81,6 +82,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) ConquestCompletedMissionsCount } = await addMissionRewards(inventory, missionReport, firstCompletion); + inventory.RewardSeed = generateRewardSeed(); await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index cb55fc69..fcb6f0a4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -750,11 +750,6 @@ export const addMissionRewards = async ( return { MissionRewards: [] }; } - if (rewardInfo.rewardSeed) { - // We're using a reward seed, so give the client a new one in the response. On live, missionInventoryUpdate seems to always provide a fresh one in the response. - inventory.RewardSeed = generateRewardSeed(); - } - //TODO: check double reward merging const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion); logger.debug("random mission drops:", MissionRewards); @@ -1423,6 +1418,11 @@ function getRandomMissionDrops( if (rewardManifests.length != 0) { logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); } + if (RewardInfo.rewardSeed) { + if (RewardInfo.rewardSeed != inventory.RewardSeed) { + logger.warn(`RewardSeed mismatch:`, { client: RewardInfo.rewardSeed, database: inventory.RewardSeed }); + } + } const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn); rewardManifests.forEach(name => { const table = ExportRewards[name]; From fb5a7320bb474f0c393a7f7ee7f767c3b77ed345 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Fri, 25 Apr 2025 11:56:27 -0700 Subject: [PATCH 605/776] chore: update to allScans cheat (#1844) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1844 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/fixed_responses/allScans.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 64eebe52..18d40763 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -1098,5 +1098,6 @@ "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem", - "/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar" + "/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar" ] From 6f64690b91d4f7f776a695a3f554fe7ff127dd97 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:56:40 -0700 Subject: [PATCH 606/776] fix: refresh duviri seed after non-quit completion of a duviri game mode (#1834) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1834 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 +++++++ src/types/requestTypes.ts | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index fcb6f0a4..32f6b35e 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -570,6 +570,13 @@ export const addMissionInventoryUpdates = async ( } break; } + case "duviriCaveOffers": { + // Duviri cave offers (generated with the duviri seed) change after completing one of its game modes (not when aborting). + if (inventoryUpdates.MissionStatus != "GS_QUIT") { + inventory.DuviriInfo.Seed = generateRewardSeed(); + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index d5b04058..f22cb298 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -127,6 +127,15 @@ export type IMissionInventoryUpdateRequest = { creditsFee?: number; // the index InvasionProgress?: IInvasionProgressClient[]; ConquestMissionsCompleted?: number; + duviriSuitSelection?: string; + duviriPistolSelection?: string; + duviriLongGunSelection?: string; + duviriMeleeSelection?: string; + duviriCaveOffers?: { + Seed: number | bigint; + Warframes: string[]; + Weapons: string[]; + }; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From 3f6734ac1c7e001240726282c9018c80cded12e9 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:00:38 -0700 Subject: [PATCH 607/776] feat(webui): EvolutionProgress support (#1818) Closes #1815 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1818 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../custom/getItemListsController.ts | 9 + .../custom/setEvolutionProgressController.ts | 33 ++++ src/routes/custom.ts | 4 +- static/fixed_responses/allIncarnonList.json | 49 ++++++ static/webui/index.html | 19 +++ static/webui/script.js | 157 +++++++++++++++++- static/webui/translations/de.js | 5 + static/webui/translations/en.js | 5 + static/webui/translations/es.js | 5 + static/webui/translations/fr.js | 5 + static/webui/translations/ru.js | 5 + static/webui/translations/zh.js | 5 + 12 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 src/controllers/custom/setEvolutionProgressController.ts create mode 100644 static/fixed_responses/allIncarnonList.json diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 38c304f7..716104a5 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -20,6 +20,7 @@ import { TRelicQuality } from "warframe-public-export-plus"; import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json"; +import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; interface ListedItem { uniqueName: string; @@ -51,6 +52,7 @@ interface ItemLists { OperatorAmps: ListedItem[]; QuestKeys: ListedItem[]; KubrowPets: ListedItem[]; + EvolutionProgress: ListedItem[]; mods: ListedItem[]; } @@ -82,6 +84,7 @@ const getItemListsController: RequestHandler = (req, response) => { OperatorAmps: [], QuestKeys: [], KubrowPets: [], + EvolutionProgress: [], mods: [] }; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { @@ -283,6 +286,12 @@ const getItemListsController: RequestHandler = (req, response) => { }); } } + for (const uniqueName of allIncarnons) { + res.EvolutionProgress.push({ + uniqueName, + name: getString(getItemName(uniqueName) || "", lang) + }); + } response.json(res); }; diff --git a/src/controllers/custom/setEvolutionProgressController.ts b/src/controllers/custom/setEvolutionProgressController.ts new file mode 100644 index 00000000..45c7c68b --- /dev/null +++ b/src/controllers/custom/setEvolutionProgressController.ts @@ -0,0 +1,33 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const setEvolutionProgressController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = req.body as ISetEvolutionProgressRequest; + + inventory.EvolutionProgress ??= []; + payload.forEach(element => { + const entry = inventory.EvolutionProgress!.find(entry => entry.ItemType === element.ItemType); + + if (entry) { + entry.Progress = 0; + entry.Rank = element.Rank; + } else { + inventory.EvolutionProgress!.push({ + Progress: 0, + Rank: element.Rank, + ItemType: element.ItemType + }); + } + }); + + await inventory.save(); + res.end(); +}; + +type ISetEvolutionProgressRequest = { + ItemType: string; + Rank: number; +}[]; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index fbd859c1..e555ef46 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -17,10 +17,11 @@ import { addCurrencyController } from "@/src/controllers/custom/addCurrencyContr import { addItemsController } from "@/src/controllers/custom/addItemsController"; import { addXpController } from "@/src/controllers/custom/addXpController"; import { importController } from "@/src/controllers/custom/importController"; +import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController"; +import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController"; -import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController"; const customRouter = express.Router(); @@ -42,6 +43,7 @@ customRouter.post("/addItems", addItemsController); customRouter.post("/addXp", addXpController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); +customRouter.post("/setEvolutionProgress", setEvolutionProgressController); customRouter.get("/config", getConfigDataController); customRouter.post("/config", updateConfigDataController); diff --git a/static/fixed_responses/allIncarnonList.json b/static/fixed_responses/allIncarnonList.json new file mode 100644 index 00000000..3d85db63 --- /dev/null +++ b/static/fixed_responses/allIncarnonList.json @@ -0,0 +1,49 @@ +[ + "/Lotus/Weapons/ClanTech/Bio/BioWeapon", + "/Lotus/Weapons/ClanTech/Energy/EnergyRifle", + "/Lotus/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", + "/Lotus/Weapons/Corpus/Pistols/CrpHandRL/CorpusHandRocketLauncher", + "/Lotus/Weapons/Grineer/LongGuns/GrineerSawbladeGun/SawBladeGun", + "/Lotus/Weapons/Grineer/Melee/GrineerTylAxeAndBoar/RegorAxeShield", + "/Lotus/Weapons/Grineer/Pistols/HeatGun/GrnHeatGun", + "/Lotus/Weapons/Infested/Pistols/InfVomitGun/InfVomitGunWep", + "/Lotus/Weapons/Syndicates/CephalonSuda/Pistols/CSDroidArray", + "/Lotus/Weapons/Tenno/Bows/HuntingBow", + "/Lotus/Weapons/Tenno/Bows/StalkerBow", + "/Lotus/Weapons/Tenno/LongGuns/TnoLeverAction/TnoLeverActionRifle", + "/Lotus/Weapons/Tenno/Melee/Axe/DualInfestedAxesWeapon", + "/Lotus/Weapons/Tenno/Melee/Dagger/CeramicDagger", + "/Lotus/Weapons/Tenno/Melee/Fist/Fist", + "/Lotus/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", + "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword", + "/Lotus/Weapons/Tenno/Melee/Maces/PaladinMace/PaladinMaceWeapon", + "/Lotus/Weapons/Tenno/Melee/Scythe/StalkerScytheWeapon", + "/Lotus/Weapons/Tenno/Melee/Scythe/ParisScythe/ParisScythe", + "/Lotus/Weapons/Tenno/Melee/Staff/Staff", + "/Lotus/Weapons/Tenno/Melee/Swords/CutlassAndPoignard/TennoCutlass", + "/Lotus/Weapons/Tenno/Melee/Swords/TennoSai/TennoSais", + "/Lotus/Weapons/Tenno/Pistol/AutoPistol", + "/Lotus/Weapons/Tenno/Pistol/BurstPistol", + "/Lotus/Weapons/Tenno/Pistol/HandShotGun", + "/Lotus/Weapons/Tenno/Pistol/HeavyPistol", + "/Lotus/Weapons/Tenno/Pistol/Pistol", + "/Lotus/Weapons/Tenno/Pistol/RevolverPistol", + "/Lotus/Weapons/Tenno/Pistols/ConclaveLeverPistol/ConclaveLeverPistol", + "/Lotus/Weapons/Tenno/Rifle/BoltoRifle", + "/Lotus/Weapons/Tenno/Rifle/BurstRifle", + "/Lotus/Weapons/Tenno/Rifle/HeavyRifle", + "/Lotus/Weapons/Tenno/Rifle/Rifle", + "/Lotus/Weapons/Tenno/Rifle/SemiAutoRifle", + "/Lotus/Weapons/Tenno/Rifle/TennoAR", + "/Lotus/Weapons/Tenno/Shotgun/FullAutoShotgun", + "/Lotus/Weapons/Tenno/Shotgun/Shotgun", + "/Lotus/Weapons/Tenno/ThrowingWeapons/Kunai", + "/Lotus/Weapons/Tenno/ThrowingWeapons/StalkerKunai", + "/Lotus/Weapons/Tenno/Zariman/LongGuns/PumpShotgun/ZarimanPumpShotgun", + "/Lotus/Weapons/Tenno/Zariman/LongGuns/SemiAutoRifle/ZarimanSemiAutoRifle", + "/Lotus/Weapons/Tenno/Zariman/Melee/Dagger/ZarimanDaggerWeapon", + "/Lotus/Weapons/Tenno/Zariman/Melee/Tonfas/ZarimanTonfaWeapon", + "/Lotus/Weapons/Tenno/Zariman/Pistols/HeavyPistol/ZarimanHeavyPistol", + "/Lotus/Weapons/Thanotech/EntFistIncarnon/EntFistIncarnon", + "/Lotus/Weapons/Thanotech/EntratiWristGun/EntratiWristGunWeapon" +] diff --git a/static/webui/index.html b/static/webui/index.html index 65a9d832..1d9f3550 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -401,6 +401,22 @@
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
@@ -411,6 +427,7 @@ +
@@ -419,6 +436,7 @@ +
@@ -733,6 +751,7 @@ + diff --git a/static/webui/script.js b/static/webui/script.js index e8dce874..ebb32177 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -183,6 +183,16 @@ const webUiModularWeapons = [ "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit" ]; +const permanentEvolutionWeapons = new Set([ + "/Lotus/Weapons/Tenno/Zariman/LongGuns/PumpShotgun/ZarimanPumpShotgun", + "/Lotus/Weapons/Tenno/Zariman/LongGuns/SemiAutoRifle/ZarimanSemiAutoRifle", + "/Lotus/Weapons/Tenno/Zariman/Melee/Dagger/ZarimanDaggerWeapon", + "/Lotus/Weapons/Tenno/Zariman/Melee/Tonfas/ZarimanTonfaWeapon", + "/Lotus/Weapons/Tenno/Zariman/Pistols/HeavyPistol/ZarimanHeavyPistol", + "/Lotus/Weapons/Thanotech/EntFistIncarnon/EntFistIncarnon", + "/Lotus/Weapons/Thanotech/EntratiWristGun/EntratiWristGunWeapon" +]); + let uniqueLevelCaps = {}; function fetchItemList() { window.itemListPromise = new Promise(resolve => { @@ -563,6 +573,82 @@ function updateInventory() { }); }); + document.getElementById("EvolutionProgress-list").innerHTML = ""; + data.EvolutionProgress.forEach(item => { + const datalist = document.getElementById("datalist-EvolutionProgress"); + const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`); + if (optionToRemove) { + datalist.removeChild(optionToRemove); + } + const tr = document.createElement("tr"); + tr.setAttribute("data-item-type", item.ItemType); + { + const td = document.createElement("td"); + td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType; + if (item.Rank != null) { + td.textContent += " | " + loc("code_rank") + ": [" + item.Rank + "/5]"; + } + tr.appendChild(td); + } + { + const td = document.createElement("td"); + td.classList = "text-end text-nowrap"; + if (item.Rank < 5) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + setEvolutionProgress([{ ItemType: item.ItemType, Rank: 5 }]); + }; + a.title = loc("code_maxRank"); + a.innerHTML = ``; + + td.appendChild(a); + } + if ((permanentEvolutionWeapons.has(item.ItemType) && item.Rank > 0) || item.Rank > 1) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + setEvolutionProgress([{ ItemType: item.ItemType, Rank: item.Rank - 1 }]); + }; + a.title = loc("code_rankDown"); + a.innerHTML = ``; + + td.appendChild(a); + } + if (item.Rank < 5) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + setEvolutionProgress([{ ItemType: item.ItemType, Rank: item.Rank + 1 }]); + }; + a.title = loc("code_rankUp"); + a.innerHTML = ``; + + td.appendChild(a); + } + + tr.appendChild(td); + } + + document.getElementById("EvolutionProgress-list").appendChild(tr); + }); + + const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option"); + const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]'); + const giveAllQEvolutionProgress = document.querySelector( + 'button[onclick*="addMissingEvolutionProgress()"]' + ); + + if (datalistEvolutionProgress.length === 0) { + formEvolutionProgress.classList.add("disabled"); + formEvolutionProgress.querySelector("input").disabled = true; + formEvolutionProgress.querySelector("button").disabled = true; + giveAllQEvolutionProgress.disabled = true; + } + // Populate quests route document.getElementById("QuestKeys-list").innerHTML = ""; data.QuestKeys.forEach(item => { @@ -674,18 +760,18 @@ function updateInventory() { }); const datalistQuestKeys = document.querySelectorAll("#datalist-QuestKeys option"); - const form = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]"); + const formQuestKeys = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]"); const giveAllQuestButton = document.querySelector("button[onclick*=\"doBulkQuestUpdate('giveAll')\"]"); if (datalistQuestKeys.length === 0) { - form.classList.add("disabled"); - form.querySelector("input").disabled = true; - form.querySelector("button").disabled = true; + formQuestKeys.classList.add("disabled"); + formQuestKeys.querySelector("input").disabled = true; + formQuestKeys.querySelector("button").disabled = true; giveAllQuestButton.disabled = true; } else { - form.classList.remove("disabled"); - form.querySelector("input").disabled = false; - form.querySelector("button").disabled = false; + formQuestKeys.classList.remove("disabled"); + formQuestKeys.querySelector("input").disabled = false; + formQuestKeys.querySelector("button").disabled = false; giveAllQuestButton.disabled = false; } @@ -1080,6 +1166,16 @@ function doAcquireModularEquipment(category, WeaponType) { } } +function doAcquireEvolution() { + const uniqueName = getKey(document.getElementById("acquire-type-EvolutionProgress")); + if (!uniqueName) { + $("#acquire-type-EvolutionProgress").addClass("is-invalid").focus(); + return; + } + + setEvolutionProgress([{ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 }]); +} + $("input[list]").on("input", function () { $(this).removeClass("is-invalid"); }); @@ -1117,6 +1213,40 @@ function addMissingEquipment(categories) { } } +function addMissingEvolutionProgress() { + const requests = []; + document.querySelectorAll("#datalist-EvolutionProgress option").forEach(elm => { + const uniqueName = elm.getAttribute("data-key"); + requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 }); + }); + if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) { + setEvolutionProgress(requests); + } +} + +function maxRankAllEvolutions() { + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + + req.done(data => { + const requests = []; + + data.EvolutionProgress.forEach(item => { + if (item.Rank < 5) { + requests.push({ + ItemType: item.ItemType, + Rank: 5 + }); + } + }); + + if (Object.keys(requests).length > 0) { + return setEvolutionProgress(requests); + } + + toast(loc("code_noEquipmentToRankUp")); + }); +} + function maxRankAllEquipment(categories) { const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); @@ -1304,6 +1434,19 @@ function maturePet(oid, revert) { }); } +function setEvolutionProgress(requests) { + revalidateAuthz(() => { + const req = $.post({ + url: "/custom/setEvolutionProgress?" + window.authz, + contentType: "application/json", + data: JSON.stringify(requests) + }); + req.done(() => { + updateInventory(); + }); + }); +} + function doAcquireMiscItems() { const uniqueName = getKey(document.getElementById("miscitem-type")); if (!uniqueName) { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 5dac7438..b0cbc3e2 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -34,6 +34,8 @@ dict = { code_rerollsNumber: `Anzahl der Umrollversuche`, code_viewStats: `Statistiken anzeigen`, code_rank: `Rang`, + code_rankUp: `[UNTRANSLATED] Rank up`, + code_rankDown: `[UNTRANSLATED] Rank down`, code_count: `Anzahl`, code_focusAllUnlocked: `Alle Fokus-Schulen sind bereits freigeschaltet.`, code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, @@ -84,18 +86,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, inventory_kubrowPets: `Bestien`, + inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, inventory_bulkAddSpaceWeapons: `Fehlende Archwing-Waffen hinzufügen`, inventory_bulkAddSentinels: `Fehlende Wächter hinzufügen`, inventory_bulkAddSentinelWeapons: `Fehlende Wächter-Waffen hinzufügen`, + inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, inventory_bulkRankUpSuits: `Alle Warframes auf Max. Rang`, inventory_bulkRankUpWeapons: `Alle Waffen auf Max. Rang`, inventory_bulkRankUpSpaceSuits: `Alle Archwings auf Max. Rang`, inventory_bulkRankUpSpaceWeapons: `Alle Archwing-Waffen auf Max. Rang`, inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`, inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`, + inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, quests_list: `Quests`, quests_completeAll: `Alle Quests abschließen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 23b8133a..3797c6d0 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -33,6 +33,8 @@ dict = { code_rerollsNumber: `Number of rerolls`, code_viewStats: `View Stats`, code_rank: `Rank`, + code_rankUp: `Rank up`, + code_rankDown: `Rank down`, code_count: `Count`, code_focusAllUnlocked: `All focus schools are already unlocked.`, code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`, @@ -83,18 +85,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, inventory_kubrowPets: `Beasts`, + inventory_evolutionProgress: `Incarnon Evolution Progress`, inventory_bulkAddSuits: `Add Missing Warframes`, inventory_bulkAddWeapons: `Add Missing Weapons`, inventory_bulkAddSpaceSuits: `Add Missing Archwings`, inventory_bulkAddSpaceWeapons: `Add Missing Archwing Weapons`, inventory_bulkAddSentinels: `Add Missing Sentinels`, inventory_bulkAddSentinelWeapons: `Add Missing Sentinel Weapons`, + inventory_bulkAddEvolutionProgress: `Add Missing Incarnon Evolution Progress`, inventory_bulkRankUpSuits: `Max Rank All Warframes`, inventory_bulkRankUpWeapons: `Max Rank All Weapons`, inventory_bulkRankUpSpaceSuits: `Max Rank All Archwings`, inventory_bulkRankUpSpaceWeapons: `Max Rank All Archwing Weapons`, inventory_bulkRankUpSentinels: `Max Rank All Sentinels`, inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`, + inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`, quests_list: `Quests`, quests_completeAll: `Complete All Quests`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index c52b68e4..7f19c5e3 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -34,6 +34,8 @@ dict = { code_rerollsNumber: `Cantidad de reintentos`, code_viewStats: `Ver estadísticas`, code_rank: `Rango`, + code_rankUp: `[UNTRANSLATED] Rank up`, + code_rankDown: `[UNTRANSLATED] Rank down`, code_count: `Cantidad`, code_focusAllUnlocked: `Todas las escuelas de enfoque ya están desbloqueadas.`, code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`, @@ -84,18 +86,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, inventory_kubrowPets: `Bestias`, + inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, inventory_bulkAddSuits: `Agregar Warframes faltantes`, inventory_bulkAddWeapons: `Agregar armas faltantes`, inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`, inventory_bulkAddSpaceWeapons: `Agregar armas Archwing faltantes`, inventory_bulkAddSentinels: `Agregar centinelas faltantes`, inventory_bulkAddSentinelWeapons: `Agregar armas de centinela faltantes`, + inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, inventory_bulkRankUpSuits: `Maximizar rango de todos los Warframes`, inventory_bulkRankUpWeapons: `Maximizar rango de todas las armas`, inventory_bulkRankUpSpaceSuits: `Maximizar rango de todos los Archwings`, inventory_bulkRankUpSpaceWeapons: `Maximizar rango de todas las armas Archwing`, inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`, inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`, + inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, quests_list: `Misiones`, quests_completeAll: `Completar todas las misiones`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 682086aa..31e3cfad 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -34,6 +34,8 @@ dict = { code_rerollsNumber: `Nombre de rerolls`, code_viewStats: `Voir les stats`, code_rank: `Rang`, + code_rankUp: `[UNTRANSLATED] Rank up`, + code_rankDown: `[UNTRANSLATED] Rank down`, code_count: `Quantité`, code_focusAllUnlocked: `Les écoles de Focus sont déjà déverrouillées.`, code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`, @@ -84,18 +86,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, inventory_kubrowPets: `Bêtes`, + inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, inventory_bulkAddWeapons: `Ajouter les armes manquantes`, inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`, inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`, inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`, + inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, inventory_bulkRankUpSuits: `Toutes les Warframes rang max`, inventory_bulkRankUpWeapons: `Toutes les armes rang max`, inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`, inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`, inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`, inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`, + inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, quests_list: `Quêtes`, quests_completeAll: `Compléter toutes les quêtes`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 959678f3..72b38741 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -34,6 +34,8 @@ dict = { code_rerollsNumber: `Количество циклов`, code_viewStats: `Просмотр характеристики`, code_rank: `Ранг`, + code_rankUp: `Повысить Ранг`, + code_rankDown: `Понизить Ранг`, code_count: `Количество`, code_focusAllUnlocked: `Все школы фокуса уже разблокированы.`, code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`, @@ -84,18 +86,21 @@ dict = { inventory_hoverboards: `К-Драйвы`, inventory_moaPets: `МОА`, inventory_kubrowPets: `Звери`, + inventory_evolutionProgress: `Прогресс эволюции Инкарнонов`, inventory_bulkAddSuits: `Добавить отсутствующие варфреймы`, inventory_bulkAddWeapons: `Добавить отсутствующее оружие`, inventory_bulkAddSpaceSuits: `Добавить отсутствующие арчвинги`, inventory_bulkAddSpaceWeapons: `Добавить отсутствующее оружие арчвингов`, inventory_bulkAddSentinels: `Добавить отсутствующих стражей`, inventory_bulkAddSentinelWeapons: `Добавить отсутствующее оружие стражей`, + inventory_bulkAddEvolutionProgress: `Добавить отсуствующий прогресс эволюции Инкарнонов`, inventory_bulkRankUpSuits: `Максимальный ранг всех варфреймов`, inventory_bulkRankUpWeapons: `Максимальный ранг всего оружия`, inventory_bulkRankUpSpaceSuits: `Максимальный ранг всех арчвингов`, inventory_bulkRankUpSpaceWeapons: `Максимальный ранг всего оружия арчвингов`, inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`, inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`, + inventory_bulkRankUpEvolutionProgress: `Максимальный ранг всех эволюций Инкарнонов`, quests_list: `Квесты`, quests_completeAll: `Завершить все квесты`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 3f0e01f5..dd2752a6 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -34,6 +34,8 @@ dict = { code_rerollsNumber: `洗卡次数`, code_viewStats: `查看属性`, code_rank: `等级`, + code_rankUp: `[UNTRANSLATED] Rank up`, + code_rankDown: `[UNTRANSLATED] Rank down`, code_count: `数量`, code_focusAllUnlocked: `所有专精学派均已解锁。`, code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, @@ -84,18 +86,21 @@ dict = { inventory_hoverboards: `K式悬浮板`, inventory_moaPets: `恐鸟`, inventory_kubrowPets: `[UNTRANSLATED] Beasts`, + inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, inventory_bulkAddSuits: `添加缺失战甲`, inventory_bulkAddWeapons: `添加缺失武器`, inventory_bulkAddSpaceSuits: `添加缺失Archwing`, inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`, inventory_bulkAddSentinels: `添加缺失守护`, inventory_bulkAddSentinelWeapons: `添加缺失守护武器`, + inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, inventory_bulkRankUpSuits: `所有战甲升满级`, inventory_bulkRankUpWeapons: `所有武器升满级`, inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`, inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`, inventory_bulkRankUpSentinels: `所有守护升满级`, inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`, + inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, quests_list: `任务`, quests_completeAll: `完成所有任务`, From 90e97d788817a02b7e790d9e170a488b0a8ee7bc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:12:45 -0700 Subject: [PATCH 608/776] chore: auto-generate guild advertisment vendor (#1845) With this, preprocessing is simplified to just refreshing expiry dates. No real change to auto-generation logic. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1845 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 68 ++++++------ src/types/vendorTypes.ts | 20 +--- .../GuildAdvertisementVendorManifest.json | 101 ------------------ 3 files changed, 32 insertions(+), 157 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index f13ca7e5..1549b763 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,13 +1,7 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; -import { - IItemManifestPreprocessed, - IRawVendorManifest, - IVendorInfo, - IVendorInfoPreprocessed, - IVendorManifestPreprocessed -} from "@/src/types/vendorTypes"; +import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { ExportVendors, IRange } from "warframe-public-export-plus"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; @@ -24,7 +18,6 @@ import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorIn import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; -import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json"; import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; @@ -39,7 +32,7 @@ import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorI import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; -const rawVendorManifests: IRawVendorManifest[] = [ +const rawVendorManifests: IVendorManifest[] = [ ArchimedeanVendorManifest, DeimosEntratiFragmentVendorProductsManifest, DeimosFishmongerVendorManifest, @@ -54,7 +47,6 @@ const rawVendorManifests: IRawVendorManifest[] = [ DuviriAcrithisVendorManifest, EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabVendorManifest, - GuildAdvertisementVendorManifest, // uses preprocessing HubsIronwakeDondaVendorManifest, // uses preprocessing HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, @@ -71,8 +63,8 @@ const rawVendorManifests: IRawVendorManifest[] = [ ]; interface IGeneratableVendorInfo extends Omit { - cycleOffset: number; - cycleDuration: number; + cycleOffset?: number; + cycleDuration?: number; } const generatableVendors: IGeneratableVendorInfo[] = [ @@ -99,9 +91,13 @@ const generatableVendors: IGeneratableVendorInfo[] = [ _id: { $oid: "5be4a159b144f3cdf1c22efa" }, TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", PropertyTextHash: "A39621049CA3CA13761028CD21C239EF", - RandomSeedType: "VRST_FLAVOUR_TEXT", - cycleOffset: 1734307200_000, - cycleDuration: unixTimesInMs.hour + RandomSeedType: "VRST_FLAVOUR_TEXT" + }, + { + _id: { $oid: "61ba123467e5d37975aeeb03" }, + TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", + PropertyTextHash: "255AFE2169BAE4130B4B20D7C55D14FA", + RandomSeedType: "VRST_FLAVOUR_TEXT" } // { // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, @@ -110,7 +106,7 @@ const generatableVendors: IGeneratableVendorInfo[] = [ // } ]; -export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => { +export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { return preprocessVendorManifest(vendorManifest); @@ -124,7 +120,7 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPr return undefined; }; -export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed | undefined => { +export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo._id.$oid == oid) { return preprocessVendorManifest(vendorManifest); @@ -138,29 +134,20 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed return undefined; }; -const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendorManifestPreprocessed => { +const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { const manifest = structuredClone(originalManifest); const info = manifest.VendorInfo; refreshExpiry(info.Expiry); for (const offer of info.ItemManifest) { - const iteration = refreshExpiry(offer.Expiry); - if (offer.ItemPrices) { - for (const price of offer.ItemPrices) { - if (typeof price.ItemType != "string") { - const itemSeed = parseInt(offer.Id.$oid.substring(16), 16); - const rng = new CRng(mixSeeds(itemSeed, iteration)); - price.ItemType = rng.randomElement(price.ItemType); - } - } - } + refreshExpiry(offer.Expiry); } - return manifest as IVendorManifestPreprocessed; + return manifest; } - return originalManifest as IVendorManifestPreprocessed; + return originalManifest; }; -const refreshExpiry = (expiry: IMongoDate): number => { +const refreshExpiry = (expiry: IMongoDate): void => { const period = parseInt(expiry.$date.$numberLong); if (Date.now() >= period) { const epoch = 1734307200_000; // Monday (for weekly schedules) @@ -168,9 +155,7 @@ const refreshExpiry = (expiry: IMongoDate): number => { const start = epoch + iteration * period; const end = start + period; expiry.$date.$numberLong = end.toString(); - return iteration; } - return 0; }; const toRange = (value: IRange | number): IRange => { @@ -180,9 +165,9 @@ const toRange = (value: IRange | number): IRange => { return value; }; -const vendorInfoCache: Record = {}; +const vendorInfoCache: Record = {}; -const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { +const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { if (!(vendorInfo.TypeName in vendorInfoCache)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; @@ -205,7 +190,9 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani // Add new offers const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16); - const cycleIndex = Math.trunc((Date.now() - vendorInfo.cycleOffset) / vendorInfo.cycleDuration); + const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000; + const cycleDuration = vendorInfo.cycleDuration ?? unixTimesInMs.hour; + const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration); const rng = new CRng(mixSeeds(vendorSeed, cycleIndex)); const manifest = ExportVendors[vendorInfo.TypeName]; const offersToAdd = []; @@ -226,14 +213,19 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani offersToAdd.push(rawItem); } } + + // For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception. + if (!manifest.isOneBinPerCycle) { + offersToAdd.reverse(); + } } - const cycleStart = vendorInfo.cycleOffset + cycleIndex * vendorInfo.cycleDuration; + const cycleStart = cycleOffset + cycleIndex * cycleDuration; for (const rawItem of offersToAdd) { const durationHoursRange = toRange(rawItem.durationHours); const expiry = cycleStart + rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; - const item: IItemManifestPreprocessed = { + const item: IItemManifest = { StoreItem: rawItem.storeItem, ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), Bin: "BIN_" + rawItem.bin, diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index a6e01835..3699b965 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -1,15 +1,11 @@ import { IMongoDate, IOid } from "./commonTypes"; export interface IItemPrice { - ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. + ItemType: string; ItemCount: number; ProductCategory: string; } -export interface IItemPricePreprocessed extends Omit { - ItemType: string; -} - export interface IItemManifest { StoreItem: string; ItemPrices?: IItemPrice[]; @@ -24,10 +20,6 @@ export interface IItemManifest { Id: IOid; } -export interface IItemManifestPreprocessed extends Omit { - ItemPrices?: IItemPricePreprocessed[]; -} - export interface IVendorInfo { _id: IOid; TypeName: string; @@ -39,14 +31,6 @@ export interface IVendorInfo { Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. } -export interface IVendorInfoPreprocessed extends Omit { - ItemManifest: IItemManifestPreprocessed[]; -} - -export interface IRawVendorManifest { +export interface IVendorManifest { VendorInfo: IVendorInfo; } - -export interface IVendorManifestPreprocessed { - VendorInfo: IVendorInfoPreprocessed; -} diff --git a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json deleted file mode 100644 index 20e3e3a3..00000000 --- a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "VendorInfo": { - "_id": { "$oid": "61ba123467e5d37975aeeb03" }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon", - "ItemPrices": [ - { - "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], - "ItemCount": 12, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [1, 1], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "604800000" } }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 79554843, - "Id": { "$oid": "67bbb592e1534511d6c1c1e2" } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain", - "ItemPrices": [ - { - "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], - "ItemCount": 7, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [1, 1], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "604800000" } }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 2413820225, - "Id": { "$oid": "67bbb592e1534511d6c1c1e3" } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm", - "ItemPrices": [ - { - "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], - "ItemCount": 3, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [1, 1], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "604800000" } }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 3262300883, - "Id": { "$oid": "67bbb592e1534511d6c1c1e4" } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow", - "ItemPrices": [ - { - "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], - "ItemCount": 20, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [1, 1], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "604800000" } }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 2797325750, - "Id": { "$oid": "67bbb592e1534511d6c1c1e5" } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost", - "ItemPrices": [ - { - "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], - "ItemCount": 10, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [1, 1], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "604800000" } }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 554932310, - "Id": { "$oid": "67bbb592e1534511d6c1c1e6" } - } - ], - "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA", - "RandomSeedType": "VRST_FLAVOUR_TEXT", - "Expiry": { "$date": { "$numberLong": "604800000" } } - } -} From c7c416c10073fb28fc24a458c973f746b3ccda73 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 00:40:57 +0200 Subject: [PATCH 609/776] chore: simplify arguments defaulted to undefined --- src/services/guildService.ts | 4 ++-- src/services/inventoryService.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 5aa7aa77..753d366d 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -134,7 +134,7 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => { export const getDojoClient = async ( guild: TGuildDatabaseDocument, status: number, - componentId: Types.ObjectId | string | undefined = undefined + componentId?: Types.ObjectId | string ): Promise => { const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, @@ -554,7 +554,7 @@ export const setGuildTechLogState = ( guild: TGuildDatabaseDocument, type: string, state: number, - dateTime: Date | undefined = undefined + dateTime?: Date ): boolean => { guild.TechChanges ??= []; const entry = guild.TechChanges.find(x => x.details == type); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 9d1bf0ed..31abb4ef 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -143,7 +143,7 @@ const awakeningRewards = [ export const addStartingGear = async ( inventory: TInventoryDatabaseDocument, - startingGear: TPartialStartingGear | undefined = undefined + startingGear?: TPartialStartingGear ): Promise => { const { LongGuns, Pistols, Suits, Melee } = startingGear || { LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], @@ -245,7 +245,7 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del export const getInventory = async ( accountOwnerId: string, - projection: string | undefined = undefined + projection?: string ): Promise => { const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId }, projection); @@ -856,7 +856,7 @@ export const addPowerSuit = async ( inventory: TInventoryDatabaseDocument, powersuitName: string, inventoryChanges: IInventoryChanges = {}, - features: number | undefined = undefined + features?: number ): Promise => { const powersuit = ExportWarframes[powersuitName] as IPowersuit | undefined; const exalted = powersuit?.exalted ?? []; @@ -888,7 +888,7 @@ export const addMechSuit = async ( inventory: TInventoryDatabaseDocument, mechsuitName: string, inventoryChanges: IInventoryChanges = {}, - features: number | undefined = undefined + features?: number ): Promise => { const powersuit = ExportWarframes[mechsuitName] as IPowersuit | undefined; const exalted = powersuit?.exalted ?? []; @@ -940,7 +940,7 @@ export const addSpaceSuit = ( inventory: TInventoryDatabaseDocument, spacesuitName: string, inventoryChanges: IInventoryChanges = {}, - features: number | undefined = undefined + features?: number ): IInventoryChanges => { const suitIndex = inventory.SpaceSuits.push({ @@ -1199,9 +1199,9 @@ export const addEquipment = ( inventory: TInventoryDatabaseDocument, category: TEquipmentKey, type: string, - modularParts: string[] | undefined = undefined, + modularParts?: string[], inventoryChanges: IInventoryChanges = {}, - defaultOverwrites: Partial | undefined = undefined + defaultOverwrites?: Partial ): IInventoryChanges => { const equipment = Object.assign( { From 2058207b6af430720cd7d44612da6963ab55b6e7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 03:15:58 +0200 Subject: [PATCH 610/776] fix: nemesis fp from client could be of type number --- src/services/importService.ts | 1 + src/types/inventoryTypes/inventoryTypes.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/services/importService.ts b/src/services/importService.ts index cde182cc..a3141073 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -169,6 +169,7 @@ const convertPendingRecipe = (client: IPendingRecipeClient): IPendingRecipeDatab const convertNemesis = (client: INemesisClient): INemesisDatabase => { return { ...client, + fp: BigInt(client.fp), d: convertDate(client.d) }; }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index b7c63a75..153021fc 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -845,7 +845,7 @@ export interface IMission extends IMissionDatabase { } export interface INemesisBaseClient { - fp: bigint; + fp: bigint | number; manifest: string; KillingSuit: string; killingDamageType: number; @@ -863,7 +863,8 @@ export interface INemesisBaseClient { Weakened: boolean; } -export interface INemesisBaseDatabase extends Omit { +export interface INemesisBaseDatabase extends Omit { + fp: bigint; d: Date; } @@ -877,7 +878,8 @@ export interface INemesisClient extends INemesisBaseClient { LastEnc: number; } -export interface INemesisDatabase extends Omit { +export interface INemesisDatabase extends Omit { + fp: bigint; d: Date; } From a1267e5f64d326d78661294f61ce2e6f7a44cdb7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:54:52 -0700 Subject: [PATCH 611/776] chore: add temple vendor manifest (#1851) Closes #1850 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1851 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 2 + .../Temple1999VendorManifest.json | 459 ++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 1549b763..d39dec09 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -29,6 +29,7 @@ import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/ge import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; +import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json"; import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; @@ -58,6 +59,7 @@ const rawVendorManifests: IVendorManifest[] = [ SolarisDebtTokenVendorRepossessionsManifest, SolarisFishmongerVendorManifest, SolarisProspectorVendorManifest, + Temple1999VendorManifest, TeshinHardModeVendorManifest, // uses preprocessing ZarimanCommisionsManifestArchimedean ]; diff --git a/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json b/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json new file mode 100644 index 00000000..6309363f --- /dev/null +++ b/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json @@ -0,0 +1,459 @@ +{ + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d56" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleBlueprint", + "ItemPrices": [ + { + "ItemCount": 195, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18c" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleSystemsBlueprint", + "ItemPrices": [ + { + "ItemCount": 65, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleChassisBlueprint", + "ItemPrices": [ + { + "ItemCount": 65, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18e" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleHelmetBlueprint", + "ItemPrices": [ + { + "ItemCount": 65, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/1999EntHybridPistolBlueprint", + "ItemPrices": [ + { + "ItemCount": 120, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c190" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolBarrelBlueprint", + "ItemPrices": [ + { + "ItemCount": 60, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c191" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolReceiverBlueprint", + "ItemPrices": [ + { + "ItemCount": 60, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c192" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolStockBlueprint", + "ItemPrices": [ + { + "ItemCount": 60, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c193" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumCoreKitA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c194" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumCymbalA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c195" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumFloorTomA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c196" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumSnareA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c197" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c198" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseB", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c199" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseC", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19a" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseD", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19b" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseE", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19c" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseF", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumSynthKeyboardA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19e" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/PhotoBooth/Vania/PhotoboothTileVaniaObjTempleDefense", + "ItemPrices": [ + { + "ItemCount": 100, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": false, + "Id": { + "$oid": "67dadc30641da66dc5c1c19f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva", + "ItemPrices": [ + { + "ItemCount": 110, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 6000, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 7, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c1a5" + } + } + ], + "PropertyTextHash": "20B13D9EB78FEC80EA32D0687F5BA1AE", + "RequiredGoalTag": "", + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + } + } +} From 13432bf0348965fe736cd7704e01e0b34ef2f2d8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:55:09 -0700 Subject: [PATCH 612/776] fix: future-proof oid string generation (#1847) This ensures they are still 24 bytes long even past the year 2106. :^) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1847 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 4 +--- src/services/worldStateService.ts | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index d39dec09..c5541b90 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -236,9 +236,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani AllowMultipurchase: false, Id: { $oid: - Math.trunc(cycleStart / 1000) - .toString(16) - .padStart(8, "0") + + ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + vendorInfo._id.$oid.substring(8, 16) + rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") + rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 4619d27a..db251d41 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -209,7 +209,7 @@ const pushSyndicateMissions = ( const dayStart = getSortieTime(day); const dayEnd = getSortieTime(day + 1); worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + idSuffix }, + _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix }, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, Tag: syndicateTag, @@ -354,7 +354,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { } worldState.Sorties.push({ - _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" }, + _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "d4d932c97c0a3acd" }, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", @@ -758,7 +758,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { const bountyCycleStart = bountyCycle * 9000000; bountyCycleEnd = bountyCycleStart + 9000000; worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, + _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000029" }, Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, Tag: "ZarimanSyndicate", @@ -766,7 +766,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Nodes: [] }); worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, + _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000004" }, Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, Tag: "EntratiLabSyndicate", @@ -774,7 +774,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Nodes: [] }); worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, + _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000006" }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "HexSyndicate", @@ -793,7 +793,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { { const rng = new CRng(seed); worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000008" }, + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "CetusSyndicate", @@ -863,7 +865,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { { const rng = new CRng(seed); worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000025" }, + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025" + }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "SolarisSyndicate", @@ -933,7 +937,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { { const rng = new CRng(seed); worldState.SyndicateMissions.push({ - _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000002" }, + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002" + }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "EntratiSyndicate", @@ -1170,7 +1176,7 @@ export const getLiteSortie = (week: number): ILiteSortie => { const weekEnd = weekStart + 604800000; return { _id: { - $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" + $oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "5e23a244740a190c" }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, From 883426e429ea309ce3a4679d775fbd3db232e319 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:26:16 -0700 Subject: [PATCH 613/776] fix: align guild advertisment vendor rotation to monday 0 UTC (#1858) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1858 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/constants/timeConstants.ts | 5 ++++- src/services/serversideVendorsService.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/constants/timeConstants.ts b/src/constants/timeConstants.ts index 32a03742..4411f556 100644 --- a/src/constants/timeConstants.ts +++ b/src/constants/timeConstants.ts @@ -2,15 +2,18 @@ const millisecondsPerSecond = 1000; const secondsPerMinute = 60; const minutesPerHour = 60; const hoursPerDay = 24; +const daysPerWeek = 7; const unixSecond = millisecondsPerSecond; const unixMinute = secondsPerMinute * millisecondsPerSecond; const unixHour = unixMinute * minutesPerHour; const unixDay = hoursPerDay * unixHour; +const unixWeek = daysPerWeek * unixDay; export const unixTimesInMs = { second: unixSecond, minute: unixMinute, hour: unixHour, - day: unixDay + day: unixDay, + week: unixWeek }; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index c5541b90..d3b5a430 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -66,7 +66,7 @@ const rawVendorManifests: IVendorManifest[] = [ interface IGeneratableVendorInfo extends Omit { cycleOffset?: number; - cycleDuration?: number; + cycleDuration: number; } const generatableVendors: IGeneratableVendorInfo[] = [ @@ -93,13 +93,15 @@ const generatableVendors: IGeneratableVendorInfo[] = [ _id: { $oid: "5be4a159b144f3cdf1c22efa" }, TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", PropertyTextHash: "A39621049CA3CA13761028CD21C239EF", - RandomSeedType: "VRST_FLAVOUR_TEXT" + RandomSeedType: "VRST_FLAVOUR_TEXT", + cycleDuration: unixTimesInMs.hour }, { _id: { $oid: "61ba123467e5d37975aeeb03" }, TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", PropertyTextHash: "255AFE2169BAE4130B4B20D7C55D14FA", - RandomSeedType: "VRST_FLAVOUR_TEXT" + RandomSeedType: "VRST_FLAVOUR_TEXT", + cycleDuration: unixTimesInMs.week } // { // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, @@ -193,7 +195,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani // Add new offers const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16); const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000; - const cycleDuration = vendorInfo.cycleDuration ?? unixTimesInMs.hour; + const cycleDuration = vendorInfo.cycleDuration; const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration); const rng = new CRng(mixSeeds(vendorSeed, cycleIndex)); const manifest = ExportVendors[vendorInfo.TypeName]; From 6f46ace40c1e052e2fdc621369f585ac9de4c1cd Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:26:52 -0700 Subject: [PATCH 614/776] fix(webui): revalidate authz for rename & delete account actions (#1860) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1860 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index ebb32177..52973d78 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -78,18 +78,22 @@ function logout() { function renameAccount() { const newname = window.prompt(loc("code_changeNameConfirm")); if (newname) { - fetch("/custom/renameAccount?" + window.authz + "&newname=" + newname).then(() => { - $(".displayname").text(newname); - updateLocElements(); + revalidateAuthz(() => { + fetch("/custom/renameAccount?" + window.authz + "&newname=" + newname).then(() => { + $(".displayname").text(newname); + updateLocElements(); + }); }); } } function deleteAccount() { if (window.confirm(loc("code_deleteAccountConfirm"))) { - fetch("/custom/deleteAccount?" + window.authz).then(() => { - logout(); - single.loadRoute("/webui/"); // Show login screen + revalidateAuthz(() => { + fetch("/custom/deleteAccount?" + window.authz).then(() => { + logout(); + single.loadRoute("/webui/"); // Show login screen + }); }); } } From f3e56480e5a0c594161934dc0bc3db673b29b083 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:26:58 -0700 Subject: [PATCH 615/776] fix(webui): error for inventory without EvolutionProgress (#1861) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1861 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index 52973d78..79b671d0 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -578,7 +578,7 @@ function updateInventory() { }); document.getElementById("EvolutionProgress-list").innerHTML = ""; - data.EvolutionProgress.forEach(item => { + data.EvolutionProgress?.forEach(item => { const datalist = document.getElementById("datalist-EvolutionProgress"); const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`); if (optionToRemove) { From 527112309056acac9a4206235949127de02eca8d Mon Sep 17 00:00:00 2001 From: hxedcl Date: Fri, 25 Apr 2025 22:13:28 -0700 Subject: [PATCH 616/776] chore(webui): update to Spanish translation (#1862) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1862 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 7f19c5e3..3770e988 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -34,8 +34,8 @@ dict = { code_rerollsNumber: `Cantidad de reintentos`, code_viewStats: `Ver estadísticas`, code_rank: `Rango`, - code_rankUp: `[UNTRANSLATED] Rank up`, - code_rankDown: `[UNTRANSLATED] Rank down`, + code_rankUp: `Subir de rango`, + code_rankDown: `Bajar de rango`, code_count: `Cantidad`, code_focusAllUnlocked: `Todas las escuelas de enfoque ya están desbloqueadas.`, code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`, @@ -86,21 +86,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, inventory_kubrowPets: `Bestias`, - inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, + inventory_evolutionProgress: `Progreso de evolución Incarnon`, inventory_bulkAddSuits: `Agregar Warframes faltantes`, inventory_bulkAddWeapons: `Agregar armas faltantes`, inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`, inventory_bulkAddSpaceWeapons: `Agregar armas Archwing faltantes`, inventory_bulkAddSentinels: `Agregar centinelas faltantes`, inventory_bulkAddSentinelWeapons: `Agregar armas de centinela faltantes`, - inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, + inventory_bulkAddEvolutionProgress: `Completar el progreso de evolución Incarnon faltante`, inventory_bulkRankUpSuits: `Maximizar rango de todos los Warframes`, inventory_bulkRankUpWeapons: `Maximizar rango de todas las armas`, inventory_bulkRankUpSpaceSuits: `Maximizar rango de todos los Archwings`, inventory_bulkRankUpSpaceWeapons: `Maximizar rango de todas las armas Archwing`, inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`, inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`, - inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, + inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`, quests_list: `Misiones`, quests_completeAll: `Completar todas las misiones`, From bbde7b2141f82dda4c878b06f9742b46cc2bdbcc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:12:54 +0200 Subject: [PATCH 617/776] chore: don't change remote/origin url --- UPDATE AND START SERVER.bat | 1 - 1 file changed, 1 deletion(-) diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index d0937399..983b8f8d 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -1,7 +1,6 @@ @echo off echo Updating SpaceNinjaServer... -git config remote.origin.url https://openwf.io/SpaceNinjaServer.git git fetch --prune git stash git reset --hard origin/main From d0c9409a2d158292ab16656438172424dce622e0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:53:41 -0700 Subject: [PATCH 618/776] fix: exclude pvp variants from daily special parts (#1846) Fixes #1836 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1846 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/modularWeaponSaleController.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index a8ddfc20..9c7c62a6 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -21,7 +21,11 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const modularWeaponSaleController: RequestHandler = async (req, res) => { const partTypeToParts: Record = {}; for (const [uniqueName, data] of Object.entries(ExportWeapons)) { - if (data.partType && data.premiumPrice) { + if ( + data.partType && + data.premiumPrice && + !data.excludeFromCodex // exclude pvp variants + ) { partTypeToParts[data.partType] ??= []; partTypeToParts[data.partType].push(uniqueName); } From a90d3a515632d4e67c6668fd967e7a84939fa56c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:54:06 -0700 Subject: [PATCH 619/776] feat: gardening (#1849) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1849 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/gardeningController.ts | 84 ++++++++++++++++++++++ src/controllers/api/getShipController.ts | 13 +++- src/models/personalRoomsModel.ts | 49 +++++++++++-- src/routes/api.ts | 2 + src/services/inventoryService.ts | 25 +++++++ src/services/personalRoomsService.ts | 70 +++++++++++++++++- src/types/missionTypes.ts | 2 + src/types/personalRoomsTypes.ts | 8 +-- src/types/shipTypes.ts | 36 +++++++--- 9 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 src/controllers/api/gardeningController.ts diff --git a/src/controllers/api/gardeningController.ts b/src/controllers/api/gardeningController.ts new file mode 100644 index 00000000..1913bd63 --- /dev/null +++ b/src/controllers/api/gardeningController.ts @@ -0,0 +1,84 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addMiscItem, getInventory } from "@/src/services/inventoryService"; +import { toStoreItem } from "@/src/services/itemDataService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IMissionReward } from "@/src/types/missionTypes"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IGardeningClient } from "@/src/types/shipTypes"; +import { RequestHandler } from "express"; +import { dict_en, ExportResources } from "warframe-public-export-plus"; + +export const gardeningController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (data.Mode != "HarvestAll") { + throw new Error(`unexpected gardening mode: ${data.Mode}`); + } + + const accountId = await getAccountIdForRequest(req); + const [inventory, personalRooms] = await Promise.all([ + getInventory(accountId, "MiscItems"), + getPersonalRooms(accountId, "Apartment") + ]); + + // Harvest plants + const inventoryChanges: IInventoryChanges = {}; + const rewards: Record = {}; + for (const planter of personalRooms.Apartment.Gardening.Planters) { + rewards[planter.Name] = []; + for (const plant of planter.Plants) { + const itemType = + "/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" + + plant.PlantType.substring(plant.PlantType.length - 1); + const itemCount = Math.random() < 0.775 ? 2 : 4; + + addMiscItem(inventory, itemType, itemCount, inventoryChanges); + + rewards[planter.Name].push([ + { + StoreItem: toStoreItem(itemType), + TypeName: itemType, + ItemCount: itemCount, + DailyCooldown: false, + Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564, + TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`, + ProductCategory: "MiscItems" + } + ]); + } + } + + // Refresh garden + personalRooms.Apartment.Gardening = createGarden(); + + await Promise.all([inventory.save(), personalRooms.save()]); + + const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1]; + const plant = planter.Plants[planter.Plants.length - 1]; + res.json({ + GardenTagName: planter.Name, + PlantType: plant.PlantType, + PlotIndex: plant.PlotIndex, + EndTime: toMongoDate(plant.EndTime), + InventoryChanges: inventoryChanges, + Gardening: personalRooms.toJSON().Apartment.Gardening, + Rewards: rewards + } satisfies IGardeningResponse); +}; + +interface IGardeningRequest { + Mode: string; +} + +interface IGardeningResponse { + GardenTagName: string; + PlantType: string; + PlotIndex: number; + EndTime: IMongoDate; + InventoryChanges: IInventoryChanges; + Gardening: IGardeningClient; + Rewards: Record; +} diff --git a/src/controllers/api/getShipController.ts b/src/controllers/api/getShipController.ts index ac1ebadf..c68ff789 100644 --- a/src/controllers/api/getShipController.ts +++ b/src/controllers/api/getShipController.ts @@ -2,17 +2,24 @@ import { RequestHandler } from "express"; import { config } from "@/src/services/configService"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getPersonalRooms } from "@/src/services/personalRoomsService"; +import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { getShip } from "@/src/services/shipService"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { IGetShipResponse } from "@/src/types/shipTypes"; -import { IPersonalRooms } from "@/src/types/personalRoomsTypes"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { getLoadout } from "@/src/services/loadoutService"; export const getShipController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const personalRoomsDb = await getPersonalRooms(accountId); - const personalRooms = personalRoomsDb.toJSON(); + + // Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future. + if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) { + personalRoomsDb.Apartment.Gardening = createGarden(); + await personalRoomsDb.save(); + } + + const personalRooms = personalRoomsDb.toJSON(); const loadout = await getLoadout(accountId); const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem"); diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index 0fcdda72..e6176ead 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -1,14 +1,17 @@ -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { colorSchema } from "@/src/models/inventoryModels/inventoryModel"; import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes"; import { IFavouriteLoadoutDatabase, - IGardening, + IGardeningDatabase, IPlacedDecosDatabase, IPictureFrameInfo, IRoom, ITailorShopDatabase, - IApartmentDatabase + IApartmentDatabase, + IPlanterDatabase, + IPlantDatabase, + IPlantClient } from "@/src/types/shipTypes"; import { Schema, model } from "mongoose"; @@ -77,15 +80,45 @@ favouriteLoadoutSchema.set("toJSON", { } }); -const gardeningSchema = new Schema({ - Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening +const plantSchema = new Schema( + { + PlantType: String, + EndTime: Date, + PlotIndex: Number + }, + { _id: false } +); + +plantSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const client = obj as IPlantClient; + const db = obj as IPlantDatabase; + + client.EndTime = toMongoDate(db.EndTime); + } }); +const planterSchema = new Schema( + { + Name: { type: String, required: true }, + Plants: { type: [plantSchema], default: [] } + }, + { _id: false } +); + +const gardeningSchema = new Schema( + { + Planters: { type: [planterSchema], default: [] } + }, + { _id: false } +); + const apartmentSchema = new Schema( { Rooms: [roomSchema], FavouriteLoadouts: [favouriteLoadoutSchema], - Gardening: gardeningSchema // TODO: ensure this is correct + Gardening: gardeningSchema }, { _id: false } ); @@ -98,7 +131,9 @@ const apartmentDefault: IApartmentDatabase = { { Name: "DuviriHallway", MaxCapacity: 1600 } ], FavouriteLoadouts: [], - Gardening: {} + Gardening: { + Planters: [] + } }; const orbiterSchema = new Schema( diff --git a/src/routes/api.ts b/src/routes/api.ts index b11af3e4..f594c845 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -48,6 +48,7 @@ import { findSessionsController } from "@/src/controllers/api/findSessionsContro import { fishmongerController } from "@/src/controllers/api/fishmongerController"; import { focusController } from "@/src/controllers/api/focusController"; import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController"; +import { gardeningController } from "@/src/controllers/api/gardeningController"; import { genericUpdateController } from "@/src/controllers/api/genericUpdateController"; import { getAllianceController } from "@/src/controllers/api/getAllianceController"; import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController"; @@ -240,6 +241,7 @@ apiRouter.post("/findSessions.php", findSessionsController); apiRouter.post("/fishmonger.php", fishmongerController); apiRouter.post("/focus.php", focusController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController); +apiRouter.post("/gardening.php", gardeningController); apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getFriends.php", getFriendsController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 31abb4ef..035984c2 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -213,6 +213,15 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del for (const key in delta) { if (!(key in InventoryChanges)) { InventoryChanges[key] = delta[key]; + } else if (key == "MiscItems") { + for (const deltaItem of delta[key]!) { + const existing = InventoryChanges[key]!.find(x => x.ItemType == deltaItem.ItemType); + if (existing) { + existing.ItemCount += deltaItem.ItemCount; + } else { + InventoryChanges[key]!.push(deltaItem); + } + } } else if (Array.isArray(delta[key])) { const left = InventoryChanges[key] as object[]; const right: object[] = delta[key]; @@ -1468,6 +1477,22 @@ export const addGearExpByCategory = ( }); }; +export const addMiscItem = ( + inventory: TInventoryDatabaseDocument, + type: string, + count: number, + inventoryChanges: IInventoryChanges +): void => { + const miscItemChanges: IMiscItem[] = [ + { + ItemType: type, + ItemCount: count + } + ]; + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); +}; + export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => { const { MiscItems } = inventory; diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 24399655..97f1b41d 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -1,9 +1,14 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { addItem, getInventory } from "@/src/services/inventoryService"; import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes"; +import { IGardeningDatabase } from "../types/shipTypes"; +import { getRandomElement } from "./rngService"; -export const getPersonalRooms = async (accountId: string): Promise => { - const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); +export const getPersonalRooms = async ( + accountId: string, + projection?: string +): Promise => { + const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection); if (!personalRooms) { throw new Error(`personal rooms not found for account ${accountId}`); @@ -25,3 +30,64 @@ export const updateShipFeature = async (accountId: string, shipFeature: string): await addItem(inventory, shipFeature, -1); await inventory.save(); }; + +export const createGarden = (): IGardeningDatabase => { + const plantTypes = [ + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantA", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantB", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantC", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantD", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantE", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantF" + ]; + const endTime = new Date((Math.trunc(Date.now() / 1000) + 79200) * 1000); // Plants will take 22 hours to grow + return { + Planters: [ + { + Name: "Garden0", + Plants: [ + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 1 + } + ] + }, + { + Name: "Garden1", + Plants: [ + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 1 + } + ] + }, + { + Name: "Garden2", + Plants: [ + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes), + EndTime: endTime, + PlotIndex: 1 + } + ] + } + ] + }; +}; diff --git a/src/types/missionTypes.ts b/src/types/missionTypes.ts index be1d08bc..3de75318 100644 --- a/src/types/missionTypes.ts +++ b/src/types/missionTypes.ts @@ -8,6 +8,8 @@ export interface IMissionReward { TypeName?: string; UpgradeLevel?: number; ItemCount: number; + DailyCooldown?: boolean; + Rarity?: number; TweetText?: string; ProductCategory?: string; FromEnemyCache?: boolean; diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index 68239bb4..325ab9e4 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -1,12 +1,12 @@ import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { - IApartment, IRoom, IPlacedDecosDatabase, ITailorShop, ITailorShopDatabase, TBootLocation, - IApartmentDatabase + IApartmentDatabase, + IApartmentClient } from "@/src/types/shipTypes"; import { Document, Model, Types } from "mongoose"; @@ -21,10 +21,10 @@ export interface IOrbiter { BootLocation?: TBootLocation; } -export interface IPersonalRooms { +export interface IPersonalRoomsClient { ShipInteriorColors: IColor; Ship: IOrbiter; - Apartment: IApartment; + Apartment: IApartmentClient; TailorShop: ITailorShop; } diff --git a/src/types/shipTypes.ts b/src/types/shipTypes.ts index a097f3ca..74eaaeae 100644 --- a/src/types/shipTypes.ts +++ b/src/types/shipTypes.ts @@ -1,12 +1,12 @@ import { Types } from "mongoose"; -import { IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ILoadoutClient } from "./saveLoadoutTypes"; export interface IGetShipResponse { ShipOwnerId: string; Ship: IShip; - Apartment: IApartment; + Apartment: IApartmentClient; TailorShop: ITailorShop; LoadOutInventory: { LoadOutPresets: ILoadoutClient }; } @@ -51,28 +51,42 @@ export interface IRoom { PlacedDecos?: IPlacedDecosDatabase[]; } -export interface IPlants { +export interface IPlantClient { PlantType: string; - EndTime: IOid; + EndTime: IMongoDate; PlotIndex: number; } -export interface IPlanters { + +export interface IPlantDatabase extends Omit { + EndTime: Date; +} + +export interface IPlanterClient { Name: string; - Plants: IPlants[]; + Plants: IPlantClient[]; } -export interface IGardening { - Planters?: IPlanters[]; +export interface IPlanterDatabase { + Name: string; + Plants: IPlantDatabase[]; } -export interface IApartment { - Gardening: IGardening; +export interface IGardeningClient { + Planters: IPlanterClient[]; +} + +export interface IGardeningDatabase { + Planters: IPlanterDatabase[]; +} + +export interface IApartmentClient { + Gardening: IGardeningClient; Rooms: IRoom[]; FavouriteLoadouts: IFavouriteLoadout[]; } export interface IApartmentDatabase { - Gardening: IGardening; + Gardening: IGardeningDatabase; Rooms: IRoom[]; FavouriteLoadouts: IFavouriteLoadoutDatabase[]; } From 95c0ad78929f4c06dcc2e208c4e0601944a3d35e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:54:25 -0700 Subject: [PATCH 620/776] fix: handle saveDialogue request without Data or Gift (#1853) Needed to just set booleans when starting dating. Closes #1852 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1853 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 49cc605c..a6a5f4f2 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -4,7 +4,6 @@ import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inven import { getAccountIdForRequest } from "@/src/services/loginService"; import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; export const saveDialogueController: RequestHandler = async (req, res) => { @@ -27,33 +26,33 @@ export const saveDialogueController: RequestHandler = async (req, res) => { const dialogue = getDialogue(inventory, request.DialogueName); dialogue.Rank = request.Rank; dialogue.Chemistry = request.Chemistry; + dialogue.QueuedDialogues = request.QueuedDialogues; + for (const bool of request.Booleans) { + dialogue.Booleans.push(bool); + if (bool == "LizzieShawzin") { + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem", + inventoryChanges + ); + } + } + for (const bool of request.ResetBooleans) { + const index = dialogue.Booleans.findIndex(x => x == bool); + if (index != -1) { + dialogue.Booleans.splice(index, 1); + } + } + for (const info of request.OtherDialogueInfos) { + const otherDialogue = getDialogue(inventory, info.Dialogue); + if (info.Tag != "") { + otherDialogue.QueuedDialogues.push(info.Tag); + } + otherDialogue.Chemistry += info.Value; // unsure + } if (request.Data) { - dialogue.QueuedDialogues = request.QueuedDialogues; - for (const bool of request.Booleans) { - dialogue.Booleans.push(bool); - if (bool == "LizzieShawzin") { - await addEmailItem( - inventory, - "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem", - inventoryChanges - ); - } - } - for (const bool of request.ResetBooleans) { - const index = dialogue.Booleans.findIndex(x => x == bool); - if (index != -1) { - dialogue.Booleans.splice(index, 1); - } - } dialogue.Completed.push(request.Data); dialogue.AvailableDate = new Date(tomorrowAt0Utc); - for (const info of request.OtherDialogueInfos) { - const otherDialogue = getDialogue(inventory, info.Dialogue); - if (info.Tag != "") { - otherDialogue.QueuedDialogues.push(info.Tag); - } - otherDialogue.Chemistry += info.Value; // unsure - } await inventory.save(); res.json({ InventoryChanges: inventoryChanges, @@ -73,8 +72,6 @@ export const saveDialogueController: RequestHandler = async (req, res) => { InventoryChanges: inventoryChanges, AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } }); - } else { - logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); } } }; From 4a6a5ea9cc4afa2d6a7ed4d29e12af2e3a079923 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:54:38 -0700 Subject: [PATCH 621/776] feat: handle WeaponSkins picked up in missions (#1854) For sigils. Closes #1839 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1854 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 ++++++ src/types/requestTypes.ts | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 32f6b35e..72813978 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -30,6 +30,7 @@ import { addMods, addRecipes, addShipDecorations, + addSkin, addStanding, combineInventoryChanges, generateRewardSeed, @@ -411,6 +412,11 @@ export const addMissionInventoryUpdates = async ( upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress }); break; + case "WeaponSkins": + for (const item of value) { + addSkin(inventory, item.ItemType); + } + break; case "Boosters": value.forEach(booster => { addBooster(booster.ItemType, booster.ExpiryDate, inventory); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index f22cb298..855b04cd 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -20,7 +20,8 @@ import { IDiscoveredMarker, ILockedWeaponGroupClient, ILoadOutPresets, - IInvasionProgressClient + IInvasionProgressClient, + IWeaponSkinClient } from "./inventoryTypes/inventoryTypes"; import { IGroup } from "./loginTypes"; @@ -101,6 +102,7 @@ export type IMissionInventoryUpdateRequest = { }[]; CollectibleScans?: ICollectibleEntry[]; Upgrades?: IUpgradeClient[]; // riven challenge progress + WeaponSkins?: IWeaponSkinClient[]; StrippedItems?: { DropTable: string; DROP_MOD?: number[]; From 48eefd8db10689f3f09dd095e66069472c8bbe81 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:54:54 -0700 Subject: [PATCH 622/776] fix: don't give droptable rewards for non-assassination sortie missions (#1855) Closes #1835 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1855 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 72813978..2283a819 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1283,6 +1283,9 @@ function getRandomMissionDrops( // Invasion assassination has Phorid has the boss who should drop Nyx parts // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"]; + } else if (RewardInfo.sortieId && region.missionIndex != 0) { + // Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. Assassinations are an exception to this. + rewardManifests = []; } else { rewardManifests = region.rewardManifests; } From 66d1a65e63bd72644d0c767b9253d43979ef59ce Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:55:03 -0700 Subject: [PATCH 623/776] fix: handle credits & platinum prices from vendors (#1856) Fixes #1837 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1856 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/purchaseService.ts | 24 ++++++++++++++++++++++++ src/types/vendorTypes.ts | 1 + 2 files changed, 25 insertions(+) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 6bf45e42..728f6570 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -66,6 +66,18 @@ export const handlePurchase = async ( if (!offer) { throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`); } + if (offer.RegularPrice) { + combineInventoryChanges( + prePurchaseInventoryChanges, + updateCurrency(inventory, offer.RegularPrice[0], false) + ); + } + if (offer.PremiumPrice) { + combineInventoryChanges( + prePurchaseInventoryChanges, + updateCurrency(inventory, offer.PremiumPrice[0], true) + ); + } if (offer.ItemPrices) { handleItemPrices( inventory, @@ -170,6 +182,9 @@ export const handlePurchase = async ( purchaseResponse.InventoryChanges, updateCurrency(inventory, offer.RegularPrice, false) ); + if (purchaseRequest.PurchaseParams.ExpectedPrice) { + throw new Error(`vendor purchase should not have an expected price`); + } const invItem: IMiscItem = { ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks", @@ -229,6 +244,12 @@ export const handlePurchase = async ( updateCurrency(inventory, offer.credits, false) ); } + if (typeof offer.platinum == "number") { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.platinum, true) + ); + } if (offer.itemPrices) { handleItemPrices( inventory, @@ -239,6 +260,9 @@ export const handlePurchase = async ( } } } + if (purchaseRequest.PurchaseParams.ExpectedPrice) { + throw new Error(`vendor purchase should not have an expected price`); + } break; case 18: { if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) { diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index 3699b965..2976ce07 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -10,6 +10,7 @@ export interface IItemManifest { StoreItem: string; ItemPrices?: IItemPrice[]; RegularPrice?: number[]; + PremiumPrice?: number[]; Bin: string; QuantityMultiplier: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. From 4d4f885c8e468dfbf72efeb2973b58b7ac212d05 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:55:45 -0700 Subject: [PATCH 624/776] feat: dontSubtractConsumables cheat (#1857) Closes #1838 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1857 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + src/services/configService.ts | 1 + src/services/missionInventoryUpdateService.ts | 9 ++++++++- static/webui/index.html | 4 ++++ 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/zh.js | 1 + 10 files changed, 20 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 32562400..4eb30783 100644 --- a/config.json.example +++ b/config.json.example @@ -19,6 +19,7 @@ "infiniteEndo": false, "infiniteRegalAya": false, "infiniteHelminthMaterials": false, + "dontSubtractConsumables": false, "unlockAllShipFeatures": false, "unlockAllShipDecorations": false, "unlockAllFlavourItems": false, diff --git a/src/services/configService.ts b/src/services/configService.ts index 24707bd5..3fe35bcb 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -24,6 +24,7 @@ interface IConfig { infiniteEndo?: boolean; infiniteRegalAya?: boolean; infiniteHelminthMaterials?: boolean; + dontSubtractConsumables?: boolean; unlockAllShipFeatures?: boolean; unlockAllShipDecorations?: boolean; unlockAllFlavourItems?: boolean; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2283a819..e758bd7f 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -266,7 +266,14 @@ export const addMissionInventoryUpdates = async ( addMiscItems(inventory, value); break; case "Consumables": - addConsumables(inventory, value); + if (config.dontSubtractConsumables) { + addConsumables( + inventory, + value.filter(x => x.ItemCount > 0) + ); + } else { + addConsumables(inventory, value); + } break; case "Recipes": addRecipes(inventory, value); diff --git a/static/webui/index.html b/static/webui/index.html index 1d9f3550..d01d49ec 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -593,6 +593,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index b0cbc3e2..47291541 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -133,6 +133,7 @@ dict = { cheats_infiniteEndo: `Unendlich Endo`, cheats_infiniteRegalAya: `Unendlich Reines Aya`, cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`, + cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`, cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`, cheats_unlockAllFlavourItems: `Alle Sammlerstücke freischalten`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 3797c6d0..3fe369a7 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -132,6 +132,7 @@ dict = { cheats_infiniteEndo: `Infinite Endo`, cheats_infiniteRegalAya: `Infinite Regal Aya`, cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`, + cheats_dontSubtractConsumables: `Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Unlock All Ship Features`, cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`, cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 3770e988..a2b29f0c 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -133,6 +133,7 @@ dict = { cheats_infiniteEndo: `Endo infinito`, cheats_infiniteRegalAya: `Aya Real infinita`, cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`, + cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`, cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`, cheats_unlockAllFlavourItems: `Desbloquear todos los ítems estéticos`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 31e3cfad..4f2bead1 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -133,6 +133,7 @@ dict = { cheats_infiniteEndo: `Endo infini`, cheats_infiniteRegalAya: `Aya Raffiné infini`, cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`, + cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`, cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`, cheats_unlockAllFlavourItems: `Débloquer tous les Flavor Items`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 72b38741..4a4b658d 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -133,6 +133,7 @@ dict = { cheats_infiniteEndo: `Бесконечное эндо`, cheats_infiniteRegalAya: `Бесконечная Королевская Айя`, cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`, + cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`, cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`, cheats_unlockAllFlavourItems: `Разблокировать все уникальные предметы`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index dd2752a6..c7d17e67 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -133,6 +133,7 @@ dict = { cheats_infiniteEndo: `无限内融核心`, cheats_infiniteRegalAya: `无限御品阿耶`, cheats_infiniteHelminthMaterials: `无限Helminth材料`, + cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `解锁所有飞船功能`, cheats_unlockAllShipDecorations: `解锁所有飞船装饰`, cheats_unlockAllFlavourItems: `解锁所有装饰物品`, From 75c011e3cbdbb71049ba2f044ec8dc3e2c2d979a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:56:06 -0700 Subject: [PATCH 625/776] fix: don't set IsNew flag for starting gear (#1859) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1859 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 9 +-- src/controllers/api/focusController.ts | 13 ++-- src/controllers/api/guildTechController.ts | 13 +--- .../api/modularWeaponCraftingController.ts | 6 +- .../api/modularWeaponSaleController.ts | 30 ++++----- src/services/inventoryService.ts | 63 +++++++++---------- 6 files changed, 56 insertions(+), 78 deletions(-) diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts index 4ab7c466..cc77589e 100644 --- a/src/controllers/api/crewShipIdentifySalvageController.ts +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -62,14 +62,7 @@ export const crewShipIdentifySalvageController: RequestHandler = async (req, res } satisfies IInnateDamageFingerprint) }; } - addEquipment( - inventory, - "CrewShipSalvagedWeapons", - payload.ItemType, - undefined, - inventoryChanges, - defaultOverwrites - ); + addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges); } inventoryChanges.CrewShipRawSalvage = [ diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index 0d2b3f65..de892829 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -104,13 +104,14 @@ export const focusController: RequestHandler = async (req, res) => { } case FocusOperation.SentTrainingAmplifier: { const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest; - const parts: string[] = [ - "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip", - "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", - "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" - ]; const inventory = await getInventory(accountId); - const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts); + const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, { + ModularParts: [ + "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" + ] + }); occupySlot(inventory, InventorySlot.AMPS, false); await inventory.save(); res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index b1612be9..821831d4 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -441,16 +441,9 @@ const finishComponentRepair = ( const inventoryChanges = { ...(category == "CrewShipWeaponSkins" ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) - : addEquipment( - inventory, - category, - salvageItem.ItemType, - undefined, - {}, - { - UpgradeFingerprint: salvageItem.UpgradeFingerprint - } - )), + : addEquipment(inventory, category, salvageItem.ItemType, { + UpgradeFingerprint: salvageItem.UpgradeFingerprint + })), ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) }; diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 91809b49..6034132c 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -36,7 +36,9 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const inventory = await getInventory(accountId); let defaultUpgrades: IDefaultUpgrade[] | undefined; - const defaultOverwrites: Partial = {}; + const defaultOverwrites: Partial = { + ModularParts: data.Parts + }; const inventoryChanges: IInventoryChanges = {}; if (category == "KubrowPets") { const traits = { @@ -151,7 +153,7 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) } } defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); - addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); + addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges); combineInventoryChanges( inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi) diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index 9c7c62a6..f60f6c3d 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -45,24 +45,18 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => { const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts); const configs = applyDefaultUpgrades(inventory, defaultUpgrades); const inventoryChanges: IInventoryChanges = { - ...addEquipment( - inventory, - category, - weaponInfo.ItemType, - weaponInfo.ModularParts, - {}, - { - Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED, - ItemName: payload.ItemName, - Configs: configs, - Polarity: [ - { - Slot: payload.PolarizeSlot, - Value: payload.PolarizeValue - } - ] - } - ), + ...addEquipment(inventory, category, weaponInfo.ItemType, { + Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED, + ItemName: payload.ItemName, + Configs: configs, + ModularParts: weaponInfo.ModularParts, + Polarity: [ + { + Slot: payload.PolarizeSlot, + Value: payload.PolarizeValue + } + ] + }), ...occupySlot(inventory, productCategoryToInventoryBin(category)!, true), ...updateCurrency(inventory, weaponInfo.PremiumPrice, true) }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 035984c2..ebce1ee6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -154,23 +154,22 @@ export const addStartingGear = async ( //TODO: properly merge weapon bin changes it is currently static here const inventoryChanges: IInventoryChanges = {}; - addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); - await addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); + addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, { IsNew: false }, inventoryChanges); + addEquipment(inventory, "Pistols", Pistols[0].ItemType, { IsNew: false }, inventoryChanges); + addEquipment(inventory, "Melee", Melee[0].ItemType, { IsNew: false }, inventoryChanges); + await addPowerSuit(inventory, Suits[0].ItemType, { IsNew: false }, inventoryChanges); addEquipment( inventory, "DataKnives", "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", - undefined, - inventoryChanges, - { XP: 450_000 } + { XP: 450_000, IsNew: false }, + inventoryChanges ); addEquipment( inventory, "Scoops", "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", - undefined, + { IsNew: false }, inventoryChanges ); @@ -531,14 +530,7 @@ export const addItem = async ( ] }); } - const inventoryChanges = addEquipment( - inventory, - weapon.productCategory, - typeName, - [], - {}, - defaultOverwrites - ); + const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName, defaultOverwrites); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); @@ -639,12 +631,9 @@ export const addItem = async ( switch (typeName.substr(1).split("/")[2]) { default: { return { - ...(await addPowerSuit( - inventory, - typeName, - {}, - premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined - )), + ...(await addPowerSuit(inventory, typeName, { + Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + })), ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) }; } @@ -864,8 +853,8 @@ const addSentinelWeapon = ( export const addPowerSuit = async ( inventory: TInventoryDatabaseDocument, powersuitName: string, - inventoryChanges: IInventoryChanges = {}, - features?: number + defaultOverwrites?: Partial, + inventoryChanges: IInventoryChanges = {} ): Promise => { const powersuit = ExportWarframes[powersuitName] as IPowersuit | undefined; const exalted = powersuit?.exalted ?? []; @@ -879,15 +868,20 @@ export const addPowerSuit = async ( } } } - const suitIndex = - inventory.Suits.push({ + const suit: Omit = Object.assign( + { ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0, - Features: features, IsNew: true - }) - 1; + }, + defaultOverwrites + ); + if (!suit.IsNew) { + suit.IsNew = undefined; + } + const suitIndex = inventory.Suits.push(suit) - 1; inventoryChanges.Suits ??= []; inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON()); return inventoryChanges; @@ -1208,20 +1202,21 @@ export const addEquipment = ( inventory: TInventoryDatabaseDocument, category: TEquipmentKey, type: string, - modularParts?: string[], - inventoryChanges: IInventoryChanges = {}, - defaultOverwrites?: Partial + defaultOverwrites?: Partial, + inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const equipment = Object.assign( + const equipment: Omit = Object.assign( { ItemType: type, Configs: [], XP: 0, - ModularParts: modularParts, - IsNew: category != "CrewShipWeapons" && category != "CrewShipSalvagedWeapons" ? true : undefined + IsNew: category != "CrewShipWeapons" && category != "CrewShipSalvagedWeapons" }, defaultOverwrites ); + if (!equipment.IsNew) { + equipment.IsNew = undefined; + } const index = inventory[category].push(equipment) - 1; inventoryChanges[category] ??= []; From ac377024684cb008042bcde7d21e08edb824b72b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:56:16 -0700 Subject: [PATCH 626/776] feat(webui): add missing max rank mods (#1863) Closes #916 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1863 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../custom/addMissingMaxRankModsController.ts | 44 +++++++++++++++++++ src/routes/custom.ts | 2 + static/webui/index.html | 3 +- static/webui/script.js | 8 ++++ static/webui/translations/de.js | 3 +- static/webui/translations/en.js | 3 +- static/webui/translations/es.js | 3 +- static/webui/translations/fr.js | 3 +- static/webui/translations/ru.js | 3 +- static/webui/translations/zh.js | 3 +- 10 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/controllers/custom/addMissingMaxRankModsController.ts diff --git a/src/controllers/custom/addMissingMaxRankModsController.ts b/src/controllers/custom/addMissingMaxRankModsController.ts new file mode 100644 index 00000000..99cd09ec --- /dev/null +++ b/src/controllers/custom/addMissingMaxRankModsController.ts @@ -0,0 +1,44 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus"; + +export const addMissingMaxRankModsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Upgrades"); + + const maxOwnedRanks: Record = {}; + for (const upgrade of inventory.Upgrades) { + const fingerprint = JSON.parse(upgrade.UpgradeFingerprint ?? "{}") as { lvl?: number }; + if (fingerprint.lvl) { + maxOwnedRanks[upgrade.ItemType] ??= 0; + if (fingerprint.lvl > maxOwnedRanks[upgrade.ItemType]) { + maxOwnedRanks[upgrade.ItemType] = fingerprint.lvl; + } + } + } + + for (const [uniqueName, data] of Object.entries(ExportUpgrades)) { + if (data.fusionLimit != 0 && data.type != "PARAZON" && maxOwnedRanks[uniqueName] != data.fusionLimit) { + inventory.Upgrades.push({ + ItemType: uniqueName, + UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit }) + }); + } + } + + for (const [uniqueName, data] of Object.entries(ExportArcanes)) { + if ( + data.name != "/Lotus/Language/Items/GenericCosmeticEnhancerName" && + maxOwnedRanks[uniqueName] != data.fusionLimit + ) { + inventory.Upgrades.push({ + ItemType: uniqueName, + UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit }) + }); + } + } + + await inventory.save(); + res.end(); +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index e555ef46..8411d996 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -10,6 +10,7 @@ import { getAccountInfoController } from "@/src/controllers/custom/getAccountInf import { renameAccountController } from "@/src/controllers/custom/renameAccountController"; import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController"; import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController"; +import { addMissingMaxRankModsController } from "@/src/controllers/custom/addMissingMaxRankModsController"; import { createAccountController } from "@/src/controllers/custom/createAccountController"; import { createMessageController } from "@/src/controllers/custom/createMessageController"; @@ -35,6 +36,7 @@ customRouter.get("/getAccountInfo", getAccountInfoController); customRouter.get("/renameAccount", renameAccountController); customRouter.get("/ircDropped", ircDroppedController); customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController); +customRouter.get("/addMissingMaxRankMods", addMissingMaxRankModsController); customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); diff --git a/static/webui/index.html b/static/webui/index.html index d01d49ec..5210cd66 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -510,8 +510,9 @@
- + +
diff --git a/static/webui/script.js b/static/webui/script.js index 79b671d0..b7c63fdf 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1795,6 +1795,14 @@ function doRemoveUnrankedMods() { }); } +function doAddMissingMaxRankMods() { + revalidateAuthz(() => { + fetch("/custom/addMissingMaxRankMods?" + window.authz).then(() => { + updateInventory(); + }); + }); +} + // Powersuit Route single.getRoute("#powersuit-route").on("beforeload", function () { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 47291541..7b21eac2 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -120,8 +120,9 @@ dict = { mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, mods_rivens: `Rivens`, mods_mods: `Mods`, - mods_bulkAddMods: `Fehlende Mods hinzufügen`, + mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, mods_removeUnranked: `Mods ohne Rang entfernen`, + mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge |DISPLAYNAME| zu administratorNames in der config.json hinzu.`, cheats_server: `Server`, cheats_skipTutorial: `Tutorial überspringen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 3fe369a7..a2c3f312 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -119,8 +119,9 @@ dict = { mods_fingerprintHelp: `Need help with the fingerprint?`, mods_rivens: `Rivens`, mods_mods: `Mods`, - mods_bulkAddMods: `Add Missing Mods`, + mods_addMissingUnrankedMods: `Add Missing Unranked Mods`, mods_removeUnranked: `Remove Unranked Mods`, + mods_addMissingMaxRankMods: `Add Missing Max Rank Mods`, cheats_administratorRequirement: `You must be an administrator to use this feature. To become an administrator, add |DISPLAYNAME| to administratorNames in the config.json.`, cheats_server: `Server`, cheats_skipTutorial: `Skip Tutorial`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index a2b29f0c..e7079188 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -120,8 +120,9 @@ dict = { mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, mods_rivens: `Agrietados`, mods_mods: `Mods`, - mods_bulkAddMods: `Agregar mods faltantes`, + mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, mods_removeUnranked: `Quitar mods sin rango`, + mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega |DISPLAYNAME| a administratorNames en el archivo config.json.`, cheats_server: `Servidor`, cheats_skipTutorial: `Omitir tutorial`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 4f2bead1..e8c6f369 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -120,8 +120,9 @@ dict = { mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, mods_rivens: `Rivens`, mods_mods: `Mods`, - mods_bulkAddMods: `Ajouter les mods manquants`, + mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, + mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez |DISPLAYNAME| à la ligne administratorNames dans le fichier config.json.`, cheats_server: `Serveur`, cheats_skipTutorial: `Passer le tutoriel`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 4a4b658d..919dc13b 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -120,8 +120,9 @@ dict = { mods_fingerprintHelp: `Нужна помощь с отпечатком?`, mods_rivens: `Моды Разлома`, mods_mods: `Моды`, - mods_bulkAddMods: `Добавить отсутствующие моды`, + mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, + mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, cheats_administratorRequirement: `Вы должны быть администратором для использования этой функции. Чтобы стать администратором, добавьте \"|DISPLAYNAME|\" в administratorNames в config.json.`, cheats_server: `Сервер`, cheats_skipTutorial: `Пропустить обучение`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index c7d17e67..4148d86b 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -120,8 +120,9 @@ dict = { mods_fingerprintHelp: `需要印记相关的帮助?`, mods_rivens: `裂罅MOD`, mods_mods: `Mods`, - mods_bulkAddMods: `添加缺失MOD`, + mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, + mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 |DISPLAYNAME| 添加到 config.json 的 administratorNames 中。`, cheats_server: `服务器`, cheats_skipTutorial: `跳过教程`, From 781f01520f40922660909952819c36bd2160c2c2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:56:22 -0700 Subject: [PATCH 627/776] feat: save lotus customization (#1864) Closes #768 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1864 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 9 +++++++-- src/services/saveLoadoutService.ts | 5 +++++ src/types/inventoryTypes/inventoryTypes.ts | 2 +- src/types/saveLoadoutTypes.ts | 4 +++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index c9dd4be8..ad1b06ad 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -96,7 +96,8 @@ import { IInvasionProgressDatabase, IInvasionProgressClient, IAccolades, - IHubNpcCustomization + IHubNpcCustomization, + ILotusCustomization } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -780,6 +781,10 @@ const loreFragmentScansSchema = new Schema( { _id: false } ); +const lotusCustomizationSchema = new Schema().add(ItemConfigSchema).add({ + Persona: String +}); + const evolutionProgressSchema = new Schema( { Progress: Number, @@ -1628,7 +1633,7 @@ const inventorySchema = new Schema( //Purchase this new permanent skin from the Lotus customization options in Personal Quarters located in your Orbiter. //https://warframe.fandom.com/wiki/Lotus#The_New_War - LotusCustomization: Schema.Types.Mixed, + LotusCustomization: { type: lotusCustomizationSchema, default: undefined }, //Progress+Rank+ItemType(ZarimanPumpShotgun) //https://warframe.fandom.com/wiki/Incarnon diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 1f860433..0676d113 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -161,6 +161,11 @@ export const handleInventoryItemConfigChange = async ( } break; } + case "LotusCustomization": { + logger.debug(`saved LotusCustomization`, equipmentChanges.LotusCustomization); + inventory.LotusCustomization = equipmentChanges.LotusCustomization; + break; + } default: { if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") { logger.debug(`general Item config saved of type ${equipmentName}`, { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 153021fc..2cec4037 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -328,7 +328,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu BlessingCooldown?: IMongoDate; CrewShipRawSalvage: ITypeCount[]; CrewMembers: ICrewMemberClient[]; - LotusCustomization: ILotusCustomization; + LotusCustomization?: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; LastInventorySync: IOid; diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index dcd1f1b8..18d692c1 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -6,7 +6,8 @@ import { ICrewShipMembersClient, ICrewShipWeapon, IFlavourItem, - ILoadoutConfigClient + ILoadoutConfigClient, + ILotusCustomization } from "./inventoryTypes/inventoryTypes"; export interface ISaveLoadoutRequest { @@ -43,6 +44,7 @@ export interface ISaveLoadoutRequest { EquippedEmotes: string[]; UseAdultOperatorLoadout: boolean; WeaponSkins: IItemEntry; + LotusCustomization: ILotusCustomization; } export type ISaveLoadoutRequestNoUpgradeVer = Omit; From d66c474bfc61ce98b698f558c94f565ccff09e50 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:56:41 -0700 Subject: [PATCH 628/776] fix: some issues with sortie generation (#1871) Now using sortieTilesets as a source of truth for allowed mission nodes as it's based only on real sorties, also added disallowed mission types for FC_OROKIN (Corrupted Vor) that otherwise cause a script error. Closes #1865 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1871 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 56 +++++++++++++++++++------------ 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index db251d41..757f809f 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -50,21 +50,27 @@ const sortieBossToFaction: Record = { SORTIE_BOSS_PHORID: "FC_INFESTATION", SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION", SORTIE_BOSS_INFALAD: "FC_INFESTATION", - SORTIE_BOSS_CORRUPTED_VOR: "FC_CORRUPTED" + SORTIE_BOSS_CORRUPTED_VOR: "FC_OROKIN" }; const sortieFactionToSystemIndexes: Record = { FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18], FC_CORPUS: [1, 4, 7, 8, 12, 15], FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15], - FC_CORRUPTED: [14] + FC_OROKIN: [14] }; const sortieFactionToFactionIndexes: Record = { FC_GRINEER: [0], FC_CORPUS: [1], FC_INFESTATION: [0, 1, 2], - FC_CORRUPTED: [3] + FC_OROKIN: [3] +}; + +const sortieFactionToSpecialMissionTileset: Record = { + FC_GRINEER: "GrineerGalleonTileset", + FC_CORPUS: "CorpusShipTileset", + FC_INFESTATION: "CorpusShipTileset" }; const sortieBossNode: Record = { @@ -273,14 +279,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { if ( sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && - !isArchwingMission(value) && - value.missionIndex != 0 && // Exclude MT_ASSASSINATION - value.missionIndex != 10 && // Exclude MT_PVP (for relays) - value.missionIndex != 21 && // Exclude MT_PURIFY - value.missionIndex != 22 && // Exclude MT_ARENA - value.missionIndex != 23 && // Exclude MT_JUNCTION - (value.missionIndex != 28 || value.systemIndex == 2) && // MT_LANDSCAPE only on Earth - value.missionIndex < 29 + key in sortieTilesets ) { if (!availableMissionIndexes.includes(value.missionIndex)) { availableMissionIndexes.push(value.missionIndex); @@ -289,21 +288,36 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { } } + const specialMissionTypes = [1, 3, 5, 9]; + if (!(sortieBossToFaction[boss] in sortieFactionToSpecialMissionTileset)) { + for (const missionType of specialMissionTypes) { + const i = availableMissionIndexes.indexOf(missionType); + if (i != -1) { + availableMissionIndexes.splice(i, 1); + } + } + } + const selectedNodes: ISortieMission[] = []; const missionTypes = new Set(); for (let i = 0; i < 3; i++) { - const randomIndex = rng.randomInt(0, nodes.length - 1); - const node = nodes[randomIndex]; - let missionIndex = ExportRegions[node].missionIndex; + let randomIndex; + let node; + let missionIndex; + do { + randomIndex = rng.randomInt(0, nodes.length - 1); + node = nodes[randomIndex]; - if ( - !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions - missionIndex != 28 && - rng.randomInt(0, 2) == 2 - ) { - missionIndex = rng.randomElement(availableMissionIndexes); - } + missionIndex = ExportRegions[node].missionIndex; + if (missionIndex != 28) { + missionIndex = rng.randomElement(availableMissionIndexes); + } + } while ( + specialMissionTypes.indexOf(missionIndex) != -1 && + sortieTilesets[node as keyof typeof sortieTilesets] != + sortieFactionToSpecialMissionTileset[sortieBossToFaction[boss]] + ); if (i == 2 && rng.randomInt(0, 2) == 2) { const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); From ca1b6c31b6e5f3bb653268161a6c88796399f416 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:57:03 -0700 Subject: [PATCH 629/776] fix: give rewards for completing a capture mission (#1872) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1872 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index e758bd7f..e4007833 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -70,16 +70,21 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] return rotations; } + const missionIndex = ExportRegions[rewardInfo.node].missionIndex; + // For Rescue missions - if (rewardInfo.node in ExportRegions && ExportRegions[rewardInfo.node].missionIndex == 3 && rewardInfo.rewardTier) { + if (rewardInfo.node in ExportRegions && missionIndex == 3 && rewardInfo.rewardTier) { return [rewardInfo.rewardTier]; } const rotationCount = rewardInfo.rewardQualifications?.length || 0; - // Empty or absent rewardQualifications should not give rewards: - // - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) + // Empty or absent rewardQualifications should not give rewards when: // - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823) + // - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) + if (rotationCount == 0 && missionIndex != 30 && missionIndex != 32) { + return [0]; + } const rotationPattern = tierOverride === undefined From de36e2ee8d82ab8fd3d1aeb1539a5ddb432ffdeb Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:14:42 -0700 Subject: [PATCH 630/776] fix: close connection for dating saveDialogue request (#1873) Missing fix for #1852 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1873 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index a6a5f4f2..14dc9aa2 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -72,6 +72,8 @@ export const saveDialogueController: RequestHandler = async (req, res) => { InventoryChanges: inventoryChanges, AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } }); + } else { + res.end(); } } }; From cf5ed0442df9fdc410194125826220fe65938b34 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 14:23:00 -0700 Subject: [PATCH 631/776] fix: don't assume rewardInfo.node is in ExportRegions (#1879) Fixes #1878 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1879 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index e4007833..c37f8843 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -70,10 +70,11 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] return rotations; } - const missionIndex = ExportRegions[rewardInfo.node].missionIndex; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const missionIndex: number | undefined = ExportRegions[rewardInfo.node]?.missionIndex; // For Rescue missions - if (rewardInfo.node in ExportRegions && missionIndex == 3 && rewardInfo.rewardTier) { + if (missionIndex == 3 && rewardInfo.rewardTier) { return [rewardInfo.rewardTier]; } From 267357871b55a06ec65d18fe1e85f23e04f73337 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 15:25:47 -0700 Subject: [PATCH 632/776] feat: handle HenchmenKilled & HintProgress incrementing (#1877) Closes #1807 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1877 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 ++++++ src/types/requestTypes.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c37f8843..3cf37bdd 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -191,6 +191,12 @@ export const addMissionInventoryUpdates = async ( if (inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } + if (inventoryUpdates.RewardInfo.NemesisHenchmenKills && inventory.Nemesis) { + inventory.Nemesis.HenchmenKilled += inventoryUpdates.RewardInfo.NemesisHenchmenKills; + } + if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) { + inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress; + } if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) { // e.g. for Profit-Taker Phase 1: // JobTier: -6, diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 855b04cd..b230213a 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -158,6 +158,8 @@ export interface IRewardInfo { lostTargetWave?: number; defenseTargetCount?: number; NemesisAbandonedRewards?: string[]; + NemesisHenchmenKills?: number; + NemesisHintProgress?: number; EOM_AFK?: number; rewardQualifications?: string; // did a Survival for 5 minutes and this was "1" PurgatoryRewardQualifications?: string; From 9993500eca5b46a5e209b4277205f0a2ef3e4d52 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Sat, 26 Apr 2025 18:53:46 -0700 Subject: [PATCH 633/776] chore(webui): update to Spanish translation (#1881) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1881 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index e7079188..d8e3f2aa 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -120,9 +120,9 @@ dict = { mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, mods_rivens: `Agrietados`, mods_mods: `Mods`, - mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, + mods_addMissingUnrankedMods: `Agregar mods sin rango faltantes`, mods_removeUnranked: `Quitar mods sin rango`, - mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, + mods_addMissingMaxRankMods: `Agregar mods de rango máximo faltantes`, cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega |DISPLAYNAME| a administratorNames en el archivo config.json.`, cheats_server: `Servidor`, cheats_skipTutorial: `Omitir tutorial`, @@ -134,7 +134,7 @@ dict = { cheats_infiniteEndo: `Endo infinito`, cheats_infiniteRegalAya: `Aya Real infinita`, cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`, - cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, + cheats_dontSubtractConsumables: `No restar consumibles`, cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`, cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`, cheats_unlockAllFlavourItems: `Desbloquear todos los ítems estéticos`, From c7658b5b20fc983bff3bd42c1e908bbb2d88fcd7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 27 Apr 2025 04:22:10 +0200 Subject: [PATCH 634/776] chore: use parallelForeach in removePigmentsFromGuildMembers --- src/services/guildService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 753d366d..80efc7b5 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -505,7 +505,7 @@ export const hasGuildPermissionEx = ( export const removePigmentsFromGuildMembers = async (guildId: string | Types.ObjectId): Promise => { const members = await GuildMember.find({ guildId, status: 0 }, "accountId"); - for (const member of members) { + await parallelForeach(members, async member => { const inventory = await getInventory(member.accountId.toString(), "MiscItems"); const index = inventory.MiscItems.findIndex( x => x.ItemType == "/Lotus/Types/Items/Research/DojoColors/GenericDojoColorPigment" @@ -514,7 +514,7 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj inventory.MiscItems.splice(index, 1); await inventory.save(); } - } + }); }; export const processGuildTechProjectContributionsUpdate = async ( From e23d865044d837a88c60b0bfa544db10e9babbd3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:27:50 -0700 Subject: [PATCH 635/776] fix: use a list of "known good" syndicate missions (#1874) Closes #1870 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1874 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 16 +- .../worldState/syndicateMissions.json | 157 ++++++++++++++++++ 2 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 static/fixed_responses/worldState/syndicateMissions.json diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 757f809f..cc93b48a 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1,5 +1,6 @@ import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json"; +import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; @@ -189,20 +190,7 @@ const pushSyndicateMissions = ( idSuffix: string, syndicateTag: string ): void => { - const nodeOptions: string[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - !isArchwingMission(value) && - !value.questReq && // Exclude zariman, murmor, and 1999 stuff - !value.hidden && // Exclude the index - !value.darkSectorData && // Exclude dark sectors - value.missionIndex != 10 && // Exclude MT_PVP (for relays) - value.missionIndex != 23 && // no junctions - value.missionIndex < 28 // no open worlds, railjack, etc - ) { - nodeOptions.push(key); - } - } + const nodeOptions: string[] = [...syndicateMissions]; const rng = new CRng(seed); const nodes: string[] = []; diff --git a/static/fixed_responses/worldState/syndicateMissions.json b/static/fixed_responses/worldState/syndicateMissions.json new file mode 100644 index 00000000..4c5b9233 --- /dev/null +++ b/static/fixed_responses/worldState/syndicateMissions.json @@ -0,0 +1,157 @@ +[ + "SettlementNode1", + "SettlementNode11", + "SettlementNode12", + "SettlementNode14", + "SettlementNode15", + "SettlementNode2", + "SettlementNode3", + "SolNode1", + "SolNode10", + "SolNode100", + "SolNode101", + "SolNode102", + "SolNode103", + "SolNode106", + "SolNode107", + "SolNode109", + "SolNode11", + "SolNode113", + "SolNode118", + "SolNode119", + "SolNode12", + "SolNode121", + "SolNode122", + "SolNode123", + "SolNode125", + "SolNode126", + "SolNode128", + "SolNode130", + "SolNode131", + "SolNode132", + "SolNode135", + "SolNode137", + "SolNode138", + "SolNode139", + "SolNode14", + "SolNode140", + "SolNode141", + "SolNode146", + "SolNode147", + "SolNode149", + "SolNode15", + "SolNode153", + "SolNode16", + "SolNode162", + "SolNode164", + "SolNode166", + "SolNode167", + "SolNode17", + "SolNode171", + "SolNode172", + "SolNode173", + "SolNode175", + "SolNode177", + "SolNode18", + "SolNode181", + "SolNode184", + "SolNode185", + "SolNode187", + "SolNode188", + "SolNode189", + "SolNode19", + "SolNode191", + "SolNode195", + "SolNode196", + "SolNode2", + "SolNode20", + "SolNode203", + "SolNode204", + "SolNode205", + "SolNode209", + "SolNode21", + "SolNode211", + "SolNode212", + "SolNode214", + "SolNode215", + "SolNode216", + "SolNode217", + "SolNode22", + "SolNode220", + "SolNode223", + "SolNode224", + "SolNode225", + "SolNode226", + "SolNode23", + "SolNode25", + "SolNode26", + "SolNode27", + "SolNode30", + "SolNode31", + "SolNode36", + "SolNode38", + "SolNode39", + "SolNode4", + "SolNode400", + "SolNode401", + "SolNode402", + "SolNode403", + "SolNode404", + "SolNode405", + "SolNode406", + "SolNode407", + "SolNode408", + "SolNode409", + "SolNode41", + "SolNode410", + "SolNode412", + "SolNode42", + "SolNode43", + "SolNode45", + "SolNode46", + "SolNode48", + "SolNode49", + "SolNode50", + "SolNode56", + "SolNode57", + "SolNode58", + "SolNode59", + "SolNode6", + "SolNode61", + "SolNode62", + "SolNode63", + "SolNode64", + "SolNode66", + "SolNode67", + "SolNode68", + "SolNode70", + "SolNode706", + "SolNode707", + "SolNode708", + "SolNode709", + "SolNode710", + "SolNode711", + "SolNode72", + "SolNode73", + "SolNode74", + "SolNode741", + "SolNode742", + "SolNode743", + "SolNode744", + "SolNode745", + "SolNode746", + "SolNode748", + "SolNode75", + "SolNode76", + "SolNode78", + "SolNode79", + "SolNode81", + "SolNode82", + "SolNode84", + "SolNode85", + "SolNode88", + "SolNode89", + "SolNode93", + "SolNode96", + "SolNode97" +] From 5cda2e2d08c41e69866d2b62658d782dd64694c4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:28:03 -0700 Subject: [PATCH 636/776] chore: improve unlockAllScans's handling of existing scans (#1875) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1875 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/stats/viewController.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/controllers/stats/viewController.ts b/src/controllers/stats/viewController.ts index 55fbbb7f..3ff5a848 100644 --- a/src/controllers/stats/viewController.ts +++ b/src/controllers/stats/viewController.ts @@ -27,7 +27,15 @@ const viewController: RequestHandler = async (req, res) => { for (const type of Object.keys(ExportEnemies.avatars)) { if (!scans.has(type)) scans.add(type); } - responseJson.Scans ??= []; + + // Take any existing scans and also set them to 9999 + if (responseJson.Scans) { + for (const scan of responseJson.Scans) { + scans.add(scan.type); + } + } + responseJson.Scans = []; + for (const type of scans) { responseJson.Scans.push({ type: type, scans: 9999 }); } From db0e0d80dd2da34cced321067644e1b2d9102069 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 27 Apr 2025 07:20:04 +0200 Subject: [PATCH 637/776] chore: remove PropertyTextHash from auto-generated vendors --- src/services/serversideVendorsService.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index d3b5a430..1ca1fa2a 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -73,7 +73,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [ { _id: { $oid: "67dadc30e4b6e0e5979c8d84" }, TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", - PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56", RandomSeedType: "VRST_WEAPON", RequiredGoalTag: "", WeaponUpgradeValueAttenuationExponent: 2.25, @@ -83,7 +82,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [ { _id: { $oid: "60ad3b6ec96976e97d227e19" }, TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", - PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70", RandomSeedType: "VRST_WEAPON", WeaponUpgradeValueAttenuationExponent: 2.25, cycleOffset: 1744934400_000, @@ -92,21 +90,18 @@ const generatableVendors: IGeneratableVendorInfo[] = [ { _id: { $oid: "5be4a159b144f3cdf1c22efa" }, TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", - PropertyTextHash: "A39621049CA3CA13761028CD21C239EF", RandomSeedType: "VRST_FLAVOUR_TEXT", cycleDuration: unixTimesInMs.hour }, { _id: { $oid: "61ba123467e5d37975aeeb03" }, TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", - PropertyTextHash: "255AFE2169BAE4130B4B20D7C55D14FA", RandomSeedType: "VRST_FLAVOUR_TEXT", cycleDuration: unixTimesInMs.week } // { // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, - // TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest", - // PropertyTextHash: "62B64A8065B7C0FA345895D4BC234621" + // TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest" // } ]; From 9e940838751b5969b5822798d58884a7ec04509e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:36:00 -0700 Subject: [PATCH 638/776] feat: handle KubrowPetEggs in missionInventoryUpdate (#1876) Closes #1866 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1876 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 10 ++++++++++ src/types/requestTypes.ts | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 3cf37bdd..a76f9292 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -513,6 +513,16 @@ export const addMissionInventoryUpdates = async ( } break; } + case "KubrowPetEggs": { + for (const egg of value) { + inventory.KubrowPetEggs ??= []; + inventory.KubrowPetEggs.push({ + ItemType: egg.ItemType, + _id: new Types.ObjectId() + }); + } + break; + } case "DiscoveredMarkers": { for (const clientMarker of value) { const dbMarker = inventory.DiscoveredMarkers.find(x => x.tag == clientMarker.tag); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index b230213a..821b4d2f 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -21,7 +21,8 @@ import { ILockedWeaponGroupClient, ILoadOutPresets, IInvasionProgressClient, - IWeaponSkinClient + IWeaponSkinClient, + IKubrowPetEggClient } from "./inventoryTypes/inventoryTypes"; import { IGroup } from "./loginTypes"; @@ -118,6 +119,7 @@ export type IMissionInventoryUpdateRequest = { NumExtraRewards: number; Count: number; }[]; + KubrowPetEggs?: IKubrowPetEggClient[]; DiscoveredMarkers?: IDiscoveredMarker[]; LockedWeaponGroup?: ILockedWeaponGroupClient; // sent when captured by zanuka UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture From ee1a49f5f2284b851e5fc25c15e7c54687384e53 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:38:48 -0700 Subject: [PATCH 639/776] feat: handle NemesisKillConvert at missionInventoryUpdate (#1880) Closes #1848 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1880 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/claimCompletedRecipeController.ts | 9 +- src/controllers/api/nemesisController.ts | 50 +----- src/helpers/nemesisHelpers.ts | 146 +++++++++++++++++- src/models/inventoryModels/inventoryModel.ts | 10 +- src/services/inventoryService.ts | 110 ++++++++++++- src/services/missionInventoryUpdateService.ts | 50 +++++- src/types/inventoryTypes/inventoryTypes.ts | 19 ++- src/types/requestTypes.ts | 11 +- 8 files changed, 344 insertions(+), 61 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index a2c8139f..f1fa6f6e 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -133,7 +133,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = if (recipe.secretIngredientAction != "SIA_UNBRAND") { InventoryChanges = { ...InventoryChanges, - ...(await addItem(inventory, recipe.resultType, recipe.num, false)) + ...(await addItem( + inventory, + recipe.resultType, + recipe.num, + false, + undefined, + pendingRecipe.TargetFingerprint + )) }; } await inventory.save(); diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 6f789e98..252092f5 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -3,6 +3,7 @@ import { encodeNemesisGuess, getInfNodes, getNemesisPasscode, + getWeaponsForManifest, IKnifeResponse } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; @@ -170,18 +171,7 @@ export const nemesisController: RequestHandler = async (req, res) => { let weaponIdx = -1; if (body.target.Faction != "FC_INFESTATION") { - let weapons: readonly string[]; - if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { - weapons = kuvaLichVersionSixWeapons; - } else if ( - body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" || - body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree" - ) { - weapons = corpusVersionThreeWeapons; - } else { - throw new Error(`unknown nemesis manifest: ${body.target.manifest}`); - } - + const weapons = getWeaponsForManifest(body.target.manifest); const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); weaponIdx = initialWeaponIdx; do { @@ -283,39 +273,3 @@ interface INemesisRequiemRequest { HiddenWhenHolstered: boolean; }; } - -const kuvaLichVersionSixWeapons = [ - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", - "/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor", - "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk", - "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken", - "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer", - "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba", - "/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher", - "/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon", - "/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind", - "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr", - "/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler", - "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek" -]; - -const corpusVersionThreeWeapons = [ - "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher", - "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor", - "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle", - "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra", - "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron", - "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron", - "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol", - "/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol", - "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon", - "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion" -]; diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index ce4190fa..6543813f 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -1,4 +1,4 @@ -import { ExportRegions } from "warframe-public-export-plus"; +import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; import { SRng } from "@/src/services/rngService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; @@ -129,3 +129,147 @@ export const consumeModCharge = ( response.UpgradeNew.push(true); } }; + +const kuvaLichVersionSixWeapons = [ + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", + "/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba", + "/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher", + "/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon", + "/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr", + "/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek" +]; + +const corpusVersionThreeWeapons = [ + "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron", + "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol", + "/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion" +]; + +export const getWeaponsForManifest = (manifest: string): readonly string[] => { + switch (manifest) { + case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": + return kuvaLichVersionSixWeapons; + case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": + case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": + return corpusVersionThreeWeapons; + } + throw new Error(`unknown nemesis manifest: ${manifest}`); +}; + +// TODO: This sucks. +export const getInnateDamageTag = ( + KillingSuit: string +): + | "InnateElectricityDamage" + | "InnateFreezeDamage" + | "InnateHeatDamage" + | "InnateImpactDamage" + | "InnateMagDamage" + | "InnateRadDamage" + | "InnateToxinDamage" => { + const baseSuitType = ExportWarframes[KillingSuit].parentName; + switch (baseSuitType) { + case "/Lotus/Powersuits/Volt/VoltBaseSuit": + case "/Lotus/Powersuits/Excalibur/ExcaliburBaseSuit": + case "/Lotus/Powersuits/AntiMatter/NovaBaseSuit": + case "/Lotus/Powersuits/Banshee/BansheeBaseSuit": + case "/Lotus/Powersuits/Berserker/BerserkerBaseSuit": + case "/Lotus/Powersuits/Magician/MagicianBaseSuit": + case "/Lotus/Powersuits/Sentient/SentientBaseSuit": + case "/Lotus/Powersuits/Gyre/GyreBaseSuit": + return "InnateElectricityDamage"; + case "/Lotus/Powersuits/Ember/EmberBaseSuit": + case "/Lotus/Powersuits/Dragon/DragonBaseSuit": + case "/Lotus/Powersuits/Nezha/NezhaBaseSuit": + case "/Lotus/Powersuits/Sandman/SandmanBaseSuit": + case "/Lotus/Powersuits/Trapper/TrapperBaseSuit": + case "/Lotus/Powersuits/Wisp/WispBaseSuit": + case "/Lotus/Powersuits/Odalisk/OdaliskBaseSuit": + case "/Lotus/Powersuits/PaxDuviricus/PaxDuviricusBaseSuit": + case "/Lotus/Powersuits/Choir/ChoirBaseSuit": + case "/Lotus/Powersuits/Temple/TempleBaseSuit": + return "InnateHeatDamage"; + case "/Lotus/Powersuits/Frost/FrostBaseSuit": + case "/Lotus/Powersuits/Glass/GlassBaseSuit": + case "/Lotus/Powersuits/Fairy/FairyBaseSuit": + case "/Lotus/Powersuits/IronFrame/IronFrameBaseSuit": + case "/Lotus/Powersuits/Revenant/RevenantBaseSuit": + case "/Lotus/Powersuits/Trinity/TrinityBaseSuit": + case "/Lotus/Powersuits/Hoplite/HopliteBaseSuit": + case "/Lotus/Powersuits/Koumei/KoumeiBaseSuit": + return "InnateFreezeDamage"; + case "/Lotus/Powersuits/Saryn/SarynBaseSuit": + case "/Lotus/Powersuits/Paladin/PaladinBaseSuit": + case "/Lotus/Powersuits/Brawler/BrawlerBaseSuit": + case "/Lotus/Powersuits/Infestation/InfestationBaseSuit": + case "/Lotus/Powersuits/Necro/NecroBaseSuit": + case "/Lotus/Powersuits/Khora/KhoraBaseSuit": + case "/Lotus/Powersuits/Ranger/RangerBaseSuit": + case "/Lotus/Powersuits/Dagath/DagathBaseSuit": + return "InnateToxinDamage"; + case "/Lotus/Powersuits/Mag/MagBaseSuit": + case "/Lotus/Powersuits/Pirate/PirateBaseSuit": + case "/Lotus/Powersuits/Cowgirl/CowgirlBaseSuit": + case "/Lotus/Powersuits/Priest/PriestBaseSuit": + case "/Lotus/Powersuits/BrokenFrame/BrokenFrameBaseSuit": + case "/Lotus/Powersuits/Alchemist/AlchemistBaseSuit": + case "/Lotus/Powersuits/Yareli/YareliBaseSuit": + case "/Lotus/Powersuits/Geode/GeodeBaseSuit": + case "/Lotus/Powersuits/Frumentarius/FrumentariusBaseSuit": + return "InnateMagDamage"; + case "/Lotus/Powersuits/Loki/LokiBaseSuit": + case "/Lotus/Powersuits/Ninja/NinjaBaseSuit": + case "/Lotus/Powersuits/Jade/JadeBaseSuit": + case "/Lotus/Powersuits/Bard/BardBaseSuit": + case "/Lotus/Powersuits/Harlequin/HarlequinBaseSuit": + case "/Lotus/Powersuits/Garuda/GarudaBaseSuit": + case "/Lotus/Powersuits/YinYang/YinYangBaseSuit": + case "/Lotus/Powersuits/Werewolf/WerewolfBaseSuit": + case "/Lotus/Powersuits/ConcreteFrame/ConcreteFrameBaseSuit": + return "InnateRadDamage"; + case "/Lotus/Powersuits/Rhino/RhinoBaseSuit": + case "/Lotus/Powersuits/Tengu/TenguBaseSuit": + case "/Lotus/Powersuits/MonkeyKing/MonkeyKingBaseSuit": + case "/Lotus/Powersuits/Runner/RunnerBaseSuit": + case "/Lotus/Powersuits/Pacifist/PacifistBaseSuit": + case "/Lotus/Powersuits/Devourer/DevourerBaseSuit": + case "/Lotus/Powersuits/Wraith/WraithBaseSuit": + case "/Lotus/Powersuits/Pagemaster/PagemasterBaseSuit": + return "InnateImpactDamage"; + } + logger.warn(`unknown innate damage type for ${KillingSuit}, using heat as a fallback`); + return "InnateHeatDamage"; +}; + +// TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003. +export const getInnateDamageValue = (fp: bigint): number => { + const rng = new SRng(fp); + rng.randomFloat(); // used for the weapon index + const WeaponUpgradeValueAttenuationExponent = 2.25; + let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent); + if (value >= 0.941428) { + value = 1; + } + return Math.trunc(value * 0x40000000); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index ad1b06ad..7dae8540 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1039,6 +1039,8 @@ const pendingRecipeSchema = new Schema( { ItemType: String, CompletionDate: Date, + TargetItemId: String, + TargetFingerprint: String, LongGuns: { type: [EquipmentSchema], default: undefined }, Pistols: { type: [EquipmentSchema], default: undefined }, Melee: { type: [EquipmentSchema], default: undefined }, @@ -1260,11 +1262,11 @@ const nemesisSchema = new Schema( PrevOwners: Number, SecondInCommand: Boolean, Weakened: Boolean, - InfNodes: [infNodeSchema], + InfNodes: { type: [infNodeSchema], default: undefined }, HenchmenKilled: Number, HintProgress: Number, - Hints: [Number], - GuessHistory: [Number], + Hints: { type: [Number], default: undefined }, + GuessHistory: { type: [Number], default: undefined }, MissionCount: Number, LastEnc: Number }, @@ -1609,7 +1611,7 @@ const inventorySchema = new Schema( //CorpusLich or GrineerLich NemesisAbandonedRewards: { type: [String], default: [] }, Nemesis: nemesisSchema, - NemesisHistory: [Schema.Types.Mixed], + NemesisHistory: { type: [nemesisSchema], default: undefined }, LastNemesisAllySpawnTime: Schema.Types.Mixed, //TradingRulesConfirmed,ShowFriendInvNotifications(Option->Social) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ebce1ee6..90ed22f5 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -26,7 +26,9 @@ import { Status, IKubrowPetDetailsDatabase, ITraits, - ICalendarProgress + ICalendarProgress, + INemesisWeaponTargetFingerprint, + INemesisPetTargetFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -79,6 +81,7 @@ import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from ". import { createMessage } from "./inboxService"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { getWorldState } from "./worldStateService"; +import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -327,7 +330,8 @@ export const addItem = async ( typeName: string, quantity: number = 1, premiumPurchase: boolean = false, - seed?: bigint + seed?: bigint, + targetFingerprint?: string ): Promise => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { @@ -530,6 +534,12 @@ export const addItem = async ( ] }); } + if (targetFingerprint) { + const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisWeaponTargetFingerprint; + defaultOverwrites.UpgradeType = targetFingerprintObj.ItemType; + defaultOverwrites.UpgradeFingerprint = JSON.stringify(targetFingerprintObj.UpgradeFingerprint); + defaultOverwrites.ItemName = targetFingerprintObj.Name; + } const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName, defaultOverwrites); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { @@ -544,6 +554,27 @@ export const addItem = async ( premiumPurchase ) }; + } else if (targetFingerprint) { + // Sister's Hound + const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisPetTargetFingerprint; + const head = targetFingerprintObj.Parts[0]; + const defaultOverwrites: Partial = { + ModularParts: targetFingerprintObj.Parts, + ItemName: targetFingerprintObj.Name, + Configs: applyDefaultUpgrades(inventory, ExportWeapons[head].defaultUpgrades) + }; + const itemType = { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + }[head] as string; + return { + ...addEquipment(inventory, "MoaPets", itemType, defaultOverwrites), + ...occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase) + }; } else { // Modular weapon parts const miscItemChanges = [ @@ -1851,3 +1882,78 @@ export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICal return inventory.CalendarProgress; }; + +export const giveNemesisWeaponRecipe = ( + inventory: TInventoryDatabaseDocument, + weaponType: string, + nemesisName: string = "AGOR ROK", + weaponLoc?: string, + KillingSuit: string = "/Lotus/Powersuits/Ember/Ember", + fp: bigint = generateRewardSeed() +): void => { + if (!weaponLoc) { + weaponLoc = ExportWeapons[weaponType].name; + } + const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == weaponType)![0]; + addRecipes(inventory, [ + { + ItemType: recipeType, + ItemCount: 1 + } + ]); + inventory.PendingRecipes.push({ + CompletionDate: new Date(), + ItemType: recipeType, + TargetFingerprint: JSON.stringify({ + ItemType: "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod", + UpgradeFingerprint: { + compat: weaponType, + buffs: [ + { + Tag: getInnateDamageTag(KillingSuit), + Value: getInnateDamageValue(fp) + } + ] + }, + Name: weaponLoc + "|" + nemesisName + } satisfies INemesisWeaponTargetFingerprint) + }); +}; + +export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => { + const head = getRandomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" + ]); + const body = getRandomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC" + ]); + const legs = getRandomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC" + ]); + const tail = getRandomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC" + ]); + const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0]; + addRecipes(inventory, [ + { + ItemType: recipeType, + ItemCount: 1 + } + ]); + inventory.PendingRecipes.push({ + CompletionDate: new Date(), + ItemType: recipeType, + TargetFingerprint: JSON.stringify({ + Parts: [head, body, legs, tail], + Name: "/Lotus/Language/Pets/ZanukaPetName|" + nemesisName + } satisfies INemesisPetTargetFingerprint) + }); +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a76f9292..3ab2d680 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -35,6 +35,8 @@ import { combineInventoryChanges, generateRewardSeed, getCalendarProgress, + giveNemesisPetRecipe, + giveNemesisWeaponRecipe, updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; @@ -53,7 +55,7 @@ import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent. import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; -import { getInfNodes } from "@/src/helpers/nemesisHelpers"; +import { getInfNodes, getWeaponsForManifest } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; @@ -612,6 +614,52 @@ export const addMissionInventoryUpdates = async ( } break; } + case "NemesisKillConvert": + if (inventory.Nemesis) { + inventory.NemesisHistory ??= []; + inventory.NemesisHistory.push({ + // Copy over all 'base' values + fp: inventory.Nemesis.fp, + d: inventory.Nemesis.d, + manifest: inventory.Nemesis.manifest, + KillingSuit: inventory.Nemesis.KillingSuit, + killingDamageType: inventory.Nemesis.killingDamageType, + ShoulderHelmet: inventory.Nemesis.ShoulderHelmet, + WeaponIdx: inventory.Nemesis.WeaponIdx, + AgentIdx: inventory.Nemesis.AgentIdx, + BirthNode: inventory.Nemesis.BirthNode, + Faction: inventory.Nemesis.Faction, + Rank: inventory.Nemesis.Rank, + Traded: inventory.Nemesis.Traded, + PrevOwners: inventory.Nemesis.PrevOwners, + SecondInCommand: inventory.Nemesis.SecondInCommand, + Weakened: inventory.Nemesis.Weakened, + // And set killed flag + k: value.killed + }); + + if (value.killed) { + if (value.weaponLoc) { + const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[ + inventory.Nemesis.WeaponIdx + ]; + giveNemesisWeaponRecipe( + inventory, + weaponType, + value.nemesisName, + value.weaponLoc, + inventory.Nemesis.KillingSuit, + inventory.Nemesis.fp + ); + } + if (value.petLoc) { + giveNemesisPetRecipe(inventory); + } + } + + inventory.Nemesis = undefined; + } + break; default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 2cec4037..e294e1ce 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -43,6 +43,7 @@ export interface IInventoryDatabase | "RecentVendorPurchases" | "NextRefill" | "Nemesis" + | "NemesisHistory" | "EntratiVaultCountResetDate" | "BrandedSuits" | "LockedWeaponGroup" @@ -79,6 +80,7 @@ export interface IInventoryDatabase RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; NextRefill?: Date; Nemesis?: INemesisDatabase; + NemesisHistory?: INemesisBaseDatabase[]; EntratiVaultCountResetDate?: Date; BrandedSuits?: Types.ObjectId[]; LockedWeaponGroup?: ILockedWeaponGroupDatabase; @@ -313,7 +315,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EquippedInstrument?: string; InvasionChainProgress: IInvasionChainProgress[]; Nemesis?: INemesisClient; - NemesisHistory: INemesisBaseClient[]; + NemesisHistory?: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; Settings?: ISettings; PersonalTechProjects: IPersonalTechProjectClient[]; @@ -902,8 +904,8 @@ export interface IPendingRecipeDatabase { ItemType: string; CompletionDate: Date; ItemId: IOid; - TargetItemId?: string; // likely related to liches - TargetFingerprint?: string; // likely related to liches + TargetItemId?: string; // unsure what this is for + TargetFingerprint?: string; LongGuns?: IEquipmentDatabase[]; Pistols?: IEquipmentDatabase[]; Melee?: IEquipmentDatabase[]; @@ -951,6 +953,17 @@ export interface ICrewShipComponentFingerprint extends IInnateDamageFingerprint SubroutineIndex?: number; } +export interface INemesisWeaponTargetFingerprint { + ItemType: string; + UpgradeFingerprint: IInnateDamageFingerprint; + Name: string; +} + +export interface INemesisPetTargetFingerprint { + Parts: string[]; + Name: string; +} + export enum GettingSlotOrderInfo { Empty = "", LotusUpgradesModsRandomizedPlayerMeleeWeaponRandomModRare0 = "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare:0", diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 821b4d2f..976c48f5 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -22,7 +22,8 @@ import { ILoadOutPresets, IInvasionProgressClient, IWeaponSkinClient, - IKubrowPetEggClient + IKubrowPetEggClient, + INemesisClient } from "./inventoryTypes/inventoryTypes"; import { IGroup } from "./loginTypes"; @@ -74,6 +75,14 @@ export type IMissionInventoryUpdateRequest = { PS: string; ActiveDojoColorResearch: string; RewardInfo?: IRewardInfo; + NemesisKillConvert?: { + nemesisName: string; + weaponLoc: string; + petLoc: "" | "/Lotus/Language/Pets/ZanukaPetName"; + fingerprint: bigint | number; + killed: boolean; + }; + target?: INemesisClient; ReceivedCeremonyMsg: boolean; LastCeremonyResetDate: number; MissionPTS: number; From afec59e8a6731429283985605f50c3ec86385bf2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:38:55 -0700 Subject: [PATCH 640/776] feat: skipClanKeyCrafting cheat (#1883) Closes #1843 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1883 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + .../api/confirmGuildInvitationController.ts | 28 ++++++++----------- src/controllers/api/createGuildController.ts | 24 +++++----------- src/services/configService.ts | 1 + src/services/guildService.ts | 28 ++++++++++++++++++- static/webui/index.html | 4 +++ 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/zh.js | 1 + 12 files changed, 57 insertions(+), 35 deletions(-) diff --git a/config.json.example b/config.json.example index 4eb30783..e8c32ed1 100644 --- a/config.json.example +++ b/config.json.example @@ -38,6 +38,7 @@ "noKimCooldowns": false, "instantResourceExtractorDrones": false, "noResourceExtractorDronesDamage": false, + "skipClanKeyCrafting": false, "noDojoRoomBuildStage": false, "noDecoBuildStage": false, "fastDojoRoomDestruction": false, diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts index 9f43b893..b4a10b98 100644 --- a/src/controllers/api/confirmGuildInvitationController.ts +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -1,8 +1,14 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Guild, GuildMember } from "@/src/models/guildModel"; import { Account } from "@/src/models/loginModel"; -import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService"; -import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; +import { + deleteGuild, + getGuildClient, + giveClanKey, + hasGuildPermission, + removeDojoKeyItems +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; import { GuildPermission } from "@/src/types/guildTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; @@ -41,14 +47,7 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r // Update inventory of new member const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); inventory.GuildId = new Types.ObjectId(req.query.clanId as string); - const recipeChanges = [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ]; - addRecipes(inventory, recipeChanges); - combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges }); + giveClanKey(inventory, inventoryChanges); await inventory.save(); const guild = (await Guild.findById(req.query.clanId as string))!; @@ -96,14 +95,9 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req, await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 }); // Update inventory of new member - const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes"); + const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes"); inventory.GuildId = new Types.ObjectId(req.query.clanId as string); - addRecipes(inventory, [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ]); + giveClanKey(inventory); await inventory.save(); // Add join to clan log diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index 9b5bc768..bbaecc19 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -2,8 +2,9 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Guild, GuildMember } from "@/src/models/guildModel"; -import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; -import { addRecipes, getInventory } from "@/src/services/inventoryService"; +import { createUniqueClanName, getGuildClient, giveClanKey } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const createGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -26,26 +27,15 @@ export const createGuildController: RequestHandler = async (req, res) => { rank: 0 }); - const inventory = await getInventory(accountId, "GuildId Recipes"); + const inventory = await getInventory(accountId, "GuildId LevelKeys Recipes"); inventory.GuildId = guild._id; - addRecipes(inventory, [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ]); + const inventoryChanges: IInventoryChanges = {}; + giveClanKey(inventory, inventoryChanges); await inventory.save(); res.json({ ...(await getGuildClient(guild, accountId)), - InventoryChanges: { - Recipes: [ - { - ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", - ItemCount: 1 - } - ] - } + InventoryChanges: inventoryChanges }); }; diff --git a/src/services/configService.ts b/src/services/configService.ts index 3fe35bcb..26a5008d 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -44,6 +44,7 @@ interface IConfig { noKimCooldowns?: boolean; instantResourceExtractorDrones?: boolean; noResourceExtractorDronesDamage?: boolean; + skipClanKeyCrafting?: boolean; noDojoRoomBuildStage?: boolean; noDojoDecoBuildStage?: boolean; fastDojoRoomDestruction?: boolean; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 80efc7b5..61e64465 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,6 +1,6 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { @@ -657,6 +657,32 @@ export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDat } }; +export const giveClanKey = (inventory: TInventoryDatabaseDocument, inventoryChanges?: IInventoryChanges): void => { + if (config.skipClanKeyCrafting) { + const levelKeyChanges = [ + { + ItemType: "/Lotus/Types/Keys/DojoKey", + ItemCount: 1 + } + ]; + addLevelKeys(inventory, levelKeyChanges); + if (inventoryChanges) { + combineInventoryChanges(inventoryChanges, { LevelKeys: levelKeyChanges }); + } + } else { + const recipeChanges = [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]; + addRecipes(inventory, recipeChanges); + if (inventoryChanges) { + combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges }); + } + } +}; + export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => { const inventoryChanges: IInventoryChanges = {}; diff --git a/static/webui/index.html b/static/webui/index.html index 5210cd66..2297c8e6 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -674,6 +674,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 7b21eac2..12ffdd25 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -154,6 +154,7 @@ dict = { cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, + cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index a2c3f312..01a4a23f 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -153,6 +153,7 @@ dict = { cheats_noKimCooldowns: `No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`, + cheats_skipClanKeyCrafting: `Skip Clan Key Crafting`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index d8e3f2aa..fbc0acc5 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -154,6 +154,7 @@ dict = { cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, + cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index e8c6f369..1dab80e6 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -154,6 +154,7 @@ dict = { cheats_noKimCooldowns: `Aucun cooldown sur le KIM`, cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`, cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`, + cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`, cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`, cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 919dc13b..d02f54a5 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -154,6 +154,7 @@ dict = { cheats_noKimCooldowns: `Чаты KIM без кулдауна`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noResourceExtractorDronesDamage: `Без урона по дронам-сборщикам`, + cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 4148d86b..2699f0a8 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -154,6 +154,7 @@ dict = { cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, + cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_fastDojoRoomDestruction: `快速拆除道场房间`, From 45748fa8be09ff532b057995ab90d859881ddb2c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 27 Apr 2025 14:20:52 -0700 Subject: [PATCH 641/776] fix: import failing for LotusCustomization from live (#1891) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1891 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 22 +++++++++++++++++--- src/services/importService.ts | 17 ++++++++++++++- src/services/inventoryService.ts | 19 +++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 7dae8540..8aa2bc42 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -781,9 +781,25 @@ const loreFragmentScansSchema = new Schema( { _id: false } ); -const lotusCustomizationSchema = new Schema().add(ItemConfigSchema).add({ - Persona: String -}); +// const lotusCustomizationSchema = new Schema().add(ItemConfigSchema).add({ +// Persona: String +// }); + +// Laxer schema for cleanupInventory +const lotusCustomizationSchema = new Schema( + { + Skins: [String], + pricol: colorSchema, + attcol: Schema.Types.Mixed, + sigcol: Schema.Types.Mixed, + eyecol: Schema.Types.Mixed, + facial: Schema.Types.Mixed, + cloth: Schema.Types.Mixed, + syancol: Schema.Types.Mixed, + Persona: String + }, + { _id: false } +); const evolutionProgressSchema = new Schema( { diff --git a/src/services/importService.ts b/src/services/importService.ts index a3141073..7fb88619 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -2,6 +2,7 @@ import { Types } from "mongoose"; import { IEquipmentClient, IEquipmentDatabase, + IItemConfig, IOperatorConfigClient, IOperatorConfigDatabase } from "../types/inventoryTypes/commonInventoryTypes"; @@ -174,6 +175,20 @@ const convertNemesis = (client: INemesisClient): INemesisDatabase => { }; }; +// Empty objects from live may have been encoded as empty arrays because of PHP. +const convertItemConfig = (client: T): T => { + return { + ...client, + pricol: Array.isArray(client.pricol) ? {} : client.pricol, + attcol: Array.isArray(client.attcol) ? {} : client.attcol, + sigcol: Array.isArray(client.sigcol) ? {} : client.sigcol, + eyecol: Array.isArray(client.eyecol) ? {} : client.eyecol, + facial: Array.isArray(client.facial) ? {} : client.facial, + cloth: Array.isArray(client.cloth) ? {} : client.cloth, + syancol: Array.isArray(client.syancol) ? {} : client.syancol + }; +}; + export const importInventory = (db: TInventoryDatabaseDocument, client: Partial): void => { for (const key of equipmentKeys) { if (client[key] !== undefined) { @@ -352,7 +367,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< db.PlayerSkills = client.PlayerSkills; } if (client.LotusCustomization !== undefined) { - db.LotusCustomization = client.LotusCustomization; + db.LotusCustomization = convertItemConfig(client.LotusCustomization); } if (client.CollectibleSeries !== undefined) { db.CollectibleSeries = client.CollectibleSeries; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 90ed22f5..c3d47ec6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1847,6 +1847,25 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => logger.debug(`removing FreeFavorsEarned from LibrarySyndicate`); LibrarySyndicate.FreeFavorsEarned = undefined; } + + if (inventory.LotusCustomization) { + if ( + Array.isArray(inventory.LotusCustomization.attcol) || + Array.isArray(inventory.LotusCustomization.sigcol) || + Array.isArray(inventory.LotusCustomization.eyecol) || + Array.isArray(inventory.LotusCustomization.facial) || + Array.isArray(inventory.LotusCustomization.cloth) || + Array.isArray(inventory.LotusCustomization.syancol) + ) { + logger.debug(`fixing empty objects represented as empty arrays in LotusCustomization`); + inventory.LotusCustomization.attcol = {}; + inventory.LotusCustomization.sigcol = {}; + inventory.LotusCustomization.eyecol = {}; + inventory.LotusCustomization.facial = {}; + inventory.LotusCustomization.cloth = {}; + inventory.LotusCustomization.syancol = {}; + } + } }; export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { From 61864b2be12855c3f8fbb105e3496604fac3fccf Mon Sep 17 00:00:00 2001 From: hxedcl Date: Sun, 27 Apr 2025 19:39:49 -0700 Subject: [PATCH 642/776] chore(webui): update to Spanish translation (#1901) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1901 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index fbc0acc5..dd4cd9ab 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -154,7 +154,7 @@ dict = { cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, - cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, + cheats_skipClanKeyCrafting: `Saltar la fabricación de la llave de clan`, cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`, cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`, cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`, From 4e3a2e17eebf3d8829797829dfb594e2b0d043d6 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Mon, 28 Apr 2025 03:36:39 -0700 Subject: [PATCH 643/776] chore: removing unnecessary entries in allScans.json (#1903) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1903 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/fixed_responses/allScans.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 18d40763..f4b43062 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -1097,7 +1097,5 @@ "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", - "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem", - "/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar", - "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar" + "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem" ] From 0d842ade90cda75ec053cdb8633f2a4e4c2cf227 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Mon, 28 Apr 2025 05:14:25 -0700 Subject: [PATCH 644/776] chore(webui): update German translation (#1904) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1904 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 12ffdd25..40b93ecb 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -34,8 +34,8 @@ dict = { code_rerollsNumber: `Anzahl der Umrollversuche`, code_viewStats: `Statistiken anzeigen`, code_rank: `Rang`, - code_rankUp: `[UNTRANSLATED] Rank up`, - code_rankDown: `[UNTRANSLATED] Rank down`, + code_rankUp: `Rang erhöhen`, + code_rankDown: `Rang verringern`, code_count: `Anzahl`, code_focusAllUnlocked: `Alle Fokus-Schulen sind bereits freigeschaltet.`, code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, @@ -86,21 +86,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moa`, inventory_kubrowPets: `Bestien`, - inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, + inventory_evolutionProgress: `Incarnon-Entwicklungsfortschritte`, inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, inventory_bulkAddSpaceWeapons: `Fehlende Archwing-Waffen hinzufügen`, inventory_bulkAddSentinels: `Fehlende Wächter hinzufügen`, inventory_bulkAddSentinelWeapons: `Fehlende Wächter-Waffen hinzufügen`, - inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, + inventory_bulkAddEvolutionProgress: `Fehlende Incarnon-Entwicklungsfortschritte hinzufügen`, inventory_bulkRankUpSuits: `Alle Warframes auf Max. Rang`, inventory_bulkRankUpWeapons: `Alle Waffen auf Max. Rang`, inventory_bulkRankUpSpaceSuits: `Alle Archwings auf Max. Rang`, inventory_bulkRankUpSpaceWeapons: `Alle Archwing-Waffen auf Max. Rang`, inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`, inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`, - inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, + inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`, quests_list: `Quests`, quests_completeAll: `Alle Quests abschließen`, @@ -120,9 +120,9 @@ dict = { mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, mods_rivens: `Rivens`, mods_mods: `Mods`, - mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, + mods_addMissingUnrankedMods: `Fehlende Mods ohne Rang hinzufügen`, mods_removeUnranked: `Mods ohne Rang entfernen`, - mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, + mods_addMissingMaxRankMods: `Fehlende Mods mit Max. Rang hinzufügen`, cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge |DISPLAYNAME| zu administratorNames in der config.json hinzu.`, cheats_server: `Server`, cheats_skipTutorial: `Tutorial überspringen`, @@ -134,7 +134,7 @@ dict = { cheats_infiniteEndo: `Unendlich Endo`, cheats_infiniteRegalAya: `Unendlich Reines Aya`, cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`, - cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, + cheats_dontSubtractConsumables: `Verbrauchsgegenstände (Ausrüstung) nicht verbrauchen`, cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`, cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`, cheats_unlockAllFlavourItems: `Alle Sammlerstücke freischalten`, @@ -154,7 +154,7 @@ dict = { cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, - cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, + cheats_skipClanKeyCrafting: `Clan-Schlüsselherstellung überspringen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, From 1e8f2fc766d1c4a839e970ae2886dc083b0c9ca5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:58:39 -0700 Subject: [PATCH 645/776] chore: comment out mixed fields in inventory (#1892) If they are needed in the future, they schould be properly schema'd. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1892 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 32 ++++++++++---------- src/types/inventoryTypes/inventoryTypes.ts | 32 ++++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 8aa2bc42..2c1b4b09 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1399,7 +1399,7 @@ const inventorySchema = new Schema( //How many Gift do you have left*(gift spends the trade) GiftsRemaining: { type: Number, default: 8 }, //Curent trade info Giving or Getting items - PendingTrades: [Schema.Types.Mixed], + //PendingTrades: [Schema.Types.Mixed], //Syndicate currently being pledged to. SupportedSyndicate: String, @@ -1449,7 +1449,7 @@ const inventorySchema = new Schema( KubrowPetEggs: [kubrowPetEggSchema], //Prints Cat(3 Prints)\Kubrow(2 Prints) Pets - KubrowPetPrints: [Schema.Types.Mixed], + //KubrowPetPrints: [Schema.Types.Mixed], //Item for EquippedGear example:Scaner,LoadoutTechSummon etc Consumables: [typeCountSchema], @@ -1495,7 +1495,7 @@ const inventorySchema = new Schema( //item like DojoKey or Boss missions key LevelKeys: [typeCountSchema], //Active quests - Quests: [Schema.Types.Mixed], + //Quests: [Schema.Types.Mixed], //Cosmetics like profile glyphs\Kavasa Prime Kubrow Collar\Game Theme etc FlavourItems: [FlavourItemSchema], @@ -1534,7 +1534,7 @@ const inventorySchema = new Schema( TauntHistory: { type: [tauntSchema], default: undefined }, //noShow2FA,VisitPrimeVault etc - WebFlags: Schema.Types.Mixed, + //WebFlags: Schema.Types.Mixed, //Id CompletedAlerts CompletedAlerts: [String], @@ -1554,7 +1554,7 @@ const inventorySchema = new Schema( //the color your clan requests like Items/Research/DojoColors/DojoColorPlainsB ActiveDojoColorResearch: String, - SentientSpawnChanceBoosters: Schema.Types.Mixed, + //SentientSpawnChanceBoosters: Schema.Types.Mixed, QualifyingInvasions: [invasionProgressSchema], FactionScores: [Number], @@ -1589,10 +1589,10 @@ const inventorySchema = new Schema( // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable DiscoveredMarkers: [discoveredMarkerSchema], //Open location mission like "JobId" + "StageCompletions" - CompletedJobs: [Schema.Types.Mixed], + //CompletedJobs: [Schema.Types.Mixed], //Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258, - PersonalGoalProgress: [Schema.Types.Mixed], + //PersonalGoalProgress: [Schema.Types.Mixed], //Setting interface Style ThemeStyle: String, @@ -1622,13 +1622,13 @@ const inventorySchema = new Schema( LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema, //https://warframe.fandom.com/wiki/Invasion - InvasionChainProgress: [Schema.Types.Mixed], + //InvasionChainProgress: [Schema.Types.Mixed], //CorpusLich or GrineerLich NemesisAbandonedRewards: { type: [String], default: [] }, Nemesis: nemesisSchema, NemesisHistory: { type: [nemesisSchema], default: undefined }, - LastNemesisAllySpawnTime: Schema.Types.Mixed, + //LastNemesisAllySpawnTime: Schema.Types.Mixed, //TradingRulesConfirmed,ShowFriendInvNotifications(Option->Social) Settings: settingsSchema, @@ -1642,7 +1642,7 @@ const inventorySchema = new Schema( PlayerSkills: { type: playerSkillsSchema, default: {} }, //TradeBannedUntil data - TradeBannedUntil: Schema.Types.Mixed, + //TradeBannedUntil: Schema.Types.Mixed, //https://warframe.fandom.com/wiki/Helminth InfestedFoundry: infestedFoundrySchema, @@ -1666,19 +1666,19 @@ const inventorySchema = new Schema( HandlerPoints: Number, ChallengesFixVersion: { type: Number, default: 6 }, PlayedParkourTutorial: Boolean, - ActiveLandscapeTraps: [Schema.Types.Mixed], - RepVotes: [Schema.Types.Mixed], - LeagueTickets: [Schema.Types.Mixed], + //ActiveLandscapeTraps: [Schema.Types.Mixed], + //RepVotes: [Schema.Types.Mixed], + //LeagueTickets: [Schema.Types.Mixed], HasContributedToDojo: Boolean, HWIDProtectEnabled: Boolean, LoadOutPresets: { type: Schema.Types.ObjectId, ref: "Loadout" }, CurrentLoadOutIds: [oidSchema], RandomUpgradesIdentified: Number, BountyScore: Number, - ChallengeInstanceStates: [Schema.Types.Mixed], + //ChallengeInstanceStates: [Schema.Types.Mixed], RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined }, - Robotics: [Schema.Types.Mixed], - UsedDailyDeals: [Schema.Types.Mixed], + //Robotics: [Schema.Types.Mixed], + //UsedDailyDeals: [Schema.Types.Mixed], CollectibleSeries: { type: [collectibleEntrySchema], default: undefined }, HasResetAccount: { type: Boolean, default: false }, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e294e1ce..3662dc23 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -258,7 +258,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EquippedGear: string[]; DeathMarks: string[]; FusionTreasures: IFusionTreasure[]; - WebFlags: IWebFlags; + //WebFlags: IWebFlags; CompletedAlerts: string[]; Consumables: ITypeCount[]; LevelKeys: ITypeCount[]; @@ -268,10 +268,10 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu KubrowPetEggs?: IKubrowPetEggClient[]; LoreFragmentScans: ILoreFragmentScan[]; EquippedEmotes: string[]; - PendingTrades: IPendingTrade[]; + //PendingTrades: IPendingTrade[]; Boosters: IBooster[]; ActiveDojoColorResearch: string; - SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; + //SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; SupportedSyndicate?: string; Affiliations: IAffiliation[]; QualifyingInvasions: IInvasionProgressClient[]; @@ -293,19 +293,19 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu ActiveAvatarImageType: string; ShipDecorations: ITypeCount[]; DiscoveredMarkers: IDiscoveredMarker[]; - CompletedJobs: ICompletedJob[]; + //CompletedJobs: ICompletedJob[]; FocusAbility?: string; FocusUpgrades: IFocusUpgrade[]; HasContributedToDojo?: boolean; HWIDProtectEnabled?: boolean; - KubrowPetPrints: IKubrowPetPrint[]; + //KubrowPetPrints: IKubrowPetPrint[]; AlignmentReplay?: IAlignment; - PersonalGoalProgress: IPersonalGoalProgress[]; + //PersonalGoalProgress: IPersonalGoalProgress[]; ThemeStyle: string; ThemeBackground: string; ThemeSounds: string; BountyScore: number; - ChallengeInstanceStates: IChallengeInstanceState[]; + //ChallengeInstanceStates: IChallengeInstanceState[]; LoginMilestoneRewards: string[]; RecentVendorPurchases?: IRecentVendorPurchaseClient[]; NodeIntrosCompleted: string[]; @@ -313,17 +313,17 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CompletedJobChains?: ICompletedJobChain[]; SeasonChallengeHistory: ISeasonChallenge[]; EquippedInstrument?: string; - InvasionChainProgress: IInvasionChainProgress[]; + //InvasionChainProgress: IInvasionChainProgress[]; Nemesis?: INemesisClient; NemesisHistory?: INemesisBaseClient[]; - LastNemesisAllySpawnTime?: IMongoDate; + //LastNemesisAllySpawnTime?: IMongoDate; Settings?: ISettings; PersonalTechProjects: IPersonalTechProjectClient[]; PlayerSkills: IPlayerSkills; CrewShipAmmo: ITypeCount[]; CrewShipWeaponSkins: IUpgradeClient[]; CrewShipSalvagedWeaponSkins: IUpgradeClient[]; - TradeBannedUntil?: IMongoDate; + //TradeBannedUntil?: IMongoDate; PlayedParkourTutorial: boolean; SubscribedToEmailsPersonalized: number; InfestedFoundry?: IInfestedFoundryClient; @@ -337,13 +337,13 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu NextRefill?: IMongoDate; FoundToday?: IMiscItem[]; // for Argon Crystals CustomMarkers?: ICustomMarkers[]; - ActiveLandscapeTraps: any[]; + //ActiveLandscapeTraps: any[]; EvolutionProgress?: IEvolutionProgress[]; - RepVotes: any[]; - LeagueTickets: any[]; - Quests: any[]; - Robotics: any[]; - UsedDailyDeals: any[]; + //RepVotes: any[]; + //LeagueTickets: any[]; + //Quests: any[]; + //Robotics: any[]; + //UsedDailyDeals: any[]; LibraryPersonalTarget?: string; LibraryPersonalProgress: ILibraryPersonalProgress[]; CollectibleSeries?: ICollectibleEntry[]; From 88d00eaaa10e78f04bf7d83d57a49956f7ede289 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:59:54 -0700 Subject: [PATCH 646/776] feat: weaken nemesis (#1893) Closes #1885 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1893 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/nemesisController.ts | 133 +++++++++++++++-------- src/helpers/nemesisHelpers.ts | 73 ++++++++++++- 2 files changed, 156 insertions(+), 50 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 252092f5..60b02a17 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -2,9 +2,12 @@ import { consumeModCharge, encodeNemesisGuess, getInfNodes, + getKnifeUpgrade, getNemesisPasscode, + getNemesisPasscodeModTypes, getWeaponsForManifest, - IKnifeResponse + IKnifeResponse, + showdownNodes } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; @@ -15,6 +18,8 @@ import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IInnateDamageFingerprint, + IInventoryClient, + INemesisClient, InventorySlot, IUpgradeClient, IWeaponSkinClient, @@ -100,50 +105,45 @@ export const nemesisController: RequestHandler = async (req, res) => { encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3) ); - // Increase antivirus - let antivirusGain = 5; - const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; - const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); - const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; - const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + // Increase antivirus if correct antivirus mod is installed const response: IKnifeResponse = {}; - for (const upgrade of body.knife!.AttachedUpgrades) { - switch (upgrade.ItemType) { - case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": - antivirusGain += 10; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; - case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": - antivirusGain += 10; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; - case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure - antivirusGain += 15; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; - case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield - antivirusGain += 15; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; - case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": - antivirusGain += 10; - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - break; + if (result1 == 0 || result2 == 0 || result3 == 0) { + let antivirusGain = 5; + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + for (const upgrade of body.knife!.AttachedUpgrades) { + switch (upgrade.ItemType) { + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + } } + inventory.Nemesis!.HenchmenKilled += antivirusGain; } - inventory.Nemesis!.HenchmenKilled += antivirusGain; + if (inventory.Nemesis!.HenchmenKilled >= 100) { inventory.Nemesis!.HenchmenKilled = 100; - inventory.Nemesis!.InfNodes = [ - { - Node: "CrewBattleNode559", - Influence: 1 - } - ]; - inventory.Nemesis!.Weakened = true; - } else { - inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0); } + inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0); await inventory.save(); res.json(response); @@ -213,6 +213,38 @@ export const nemesisController: RequestHandler = async (req, res) => { res.json({ target: inventory.toJSON().Nemesis }); + } else if ((req.query.mode as string) == "w") { + const inventory = await getInventory( + accountId, + "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" + ); + //const body = getJSONfromString(String(req.body)); + + inventory.Nemesis!.InfNodes = [ + { + Node: showdownNodes[inventory.Nemesis!.Faction], + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + + const response: IKnifeResponse & { target: INemesisClient } = { + target: inventory.toJSON().Nemesis! + }; + + // Consume charge of the correct requiem mod(s) + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!); + for (const modType of modTypes) { + const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType); + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + } + + await inventory.save(); + res.json(response); } else { logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`); @@ -264,12 +296,19 @@ interface INemesisRequiemRequest { guess: number; // grn/crp: 4 bits | coda: 3x 4 bits position: number; // grn/crp: 0-2 | coda: 0 // knife field provided for coda only - knife?: { - Item: IEquipmentClient; - Skins: IWeaponSkinClient[]; - ModSlot: number; - CustSlot: number; - AttachedUpgrades: IUpgradeClient[]; - HiddenWhenHolstered: boolean; - }; + knife?: IKnife; +} + +// interface INemesisWeakenRequest { +// target: INemesisClient; +// knife: IKnife; +// } + +interface IKnife { + Item: IEquipmentClient; + Skins: IWeaponSkinClient[]; + ModSlot: number; + CustSlot: number; + AttachedUpgrades: IUpgradeClient[]; + HiddenWhenHolstered: boolean; } diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 6543813f..17a2d3f5 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -38,17 +38,59 @@ const systemIndexes: Record = { FC_INFESTATION: [23] }; +export const showdownNodes: Record = { + FC_GRINEER: "CrewBattleNode557", + FC_CORPUS: "CrewBattleNode558", + FC_INFESTATION: "CrewBattleNode559" +}; + // Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => { const rng = new SRng(nemesis.fp); - const passcode = [rng.randomInt(0, 7)]; + const choices = [0, 1, 2, 3, 5, 6, 7]; + let choiceIndex = rng.randomInt(0, choices.length - 1); + const passcode = [choices[choiceIndex]]; if (nemesis.Faction != "FC_INFESTATION") { - passcode.push(rng.randomInt(0, 7)); - passcode.push(rng.randomInt(0, 7)); + choices.splice(choiceIndex, 1); + choiceIndex = rng.randomInt(0, choices.length - 1); + passcode.push(choices[choiceIndex]); + + choices.splice(choiceIndex, 1); + choiceIndex = rng.randomInt(0, choices.length - 1); + passcode.push(choices[choiceIndex]); } return passcode; }; +const reqiuemMods: readonly string[] = [ + "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod" +]; + +const antivirusMods: readonly string[] = [ + "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" +]; + +export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => { + const passcode = getNemesisPasscode(nemesis); + return nemesis.Faction == "FC_INFESTATION" + ? passcode.map(i => antivirusMods[i]) + : passcode.map(i => reqiuemMods[i]); +}; + export const encodeNemesisGuess = ( symbol1: number, result1: number, @@ -79,6 +121,31 @@ export interface IKnifeResponse { HasKnife?: boolean; } +export const getKnifeUpgrade = ( + inventory: TInventoryDatabaseDocument, + dataknifeUpgrades: string[], + type: string +): { ItemId: IOid; ItemType: string } => { + if (dataknifeUpgrades.indexOf(type) != -1) { + return { + ItemId: { $oid: "000000000000000000000000" }, + ItemType: type + }; + } + for (const upgradeId of dataknifeUpgrades) { + if (upgradeId.length == 24) { + const upgrade = inventory.Upgrades.id(upgradeId); + if (upgrade && upgrade.ItemType == type) { + return { + ItemId: { $oid: upgradeId }, + ItemType: type + }; + } + } + } + throw new Error(`${type} does not seem to be installed on parazon?!`); +}; + export const consumeModCharge = ( response: IKnifeResponse, inventory: TInventoryDatabaseDocument, From 7a295a86ec4fb8919d60489b6a2664623f5496b1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:00:06 -0700 Subject: [PATCH 647/776] fix: handle boosters in store item utilities (#1894) e.g. `/Lotus/Types/StoreItems/Boosters/AffinityBoosterStoreItem` Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1894 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/itemDataService.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 6e0edd23..3b3e997c 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -17,6 +17,7 @@ import { dict_uk, dict_zh, ExportArcanes, + ExportBoosters, ExportCustoms, ExportDrones, ExportGear, @@ -217,15 +218,30 @@ export const convertInboxMessage = (message: IInboxMessage): IMessage => { }; export const isStoreItem = (type: string): boolean => { - return type.startsWith("/Lotus/StoreItems/"); + return type.startsWith("/Lotus/StoreItems/") || type in ExportBoosters; }; export const toStoreItem = (type: string): string => { + if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) { + const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type); + if (boosterEntry) { + return boosterEntry[0]; + } + throw new Error(`could not convert ${type} to a store item`); + } return "/Lotus/StoreItems/" + type.substring("/Lotus/".length); }; export const fromStoreItem = (type: string): string => { - return "/Lotus/" + type.substring("/Lotus/StoreItems/".length); + if (type.startsWith("/Lotus/StoreItems/")) { + return "/Lotus/" + type.substring("/Lotus/StoreItems/".length); + } + + if (type in ExportBoosters) { + return ExportBoosters[type].typeName; + } + + throw new Error(`${type} is not a store item`); }; export const getDefaultUpgrades = (parts: string[]): IDefaultUpgrade[] | undefined => { From 66ee550ccd94e16d91d17e4562a9caa937e57750 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:00:22 -0700 Subject: [PATCH 648/776] feat: refresh duviri seed when mood changes (#1895) Closes #1887 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1895 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 | 21 ++++++++++++++++---- src/models/inventoryModels/inventoryModel.ts | 4 ++++ src/types/inventoryTypes/inventoryTypes.ts | 4 +++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index ab739c4c..e9b15a63 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -18,10 +18,12 @@ import { addMiscItems, allDailyAffiliationKeys, cleanupInventory, - createLibraryDailyTask + createLibraryDailyTask, + generateRewardSeed } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { Types } from "mongoose"; export const inventoryController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -87,7 +89,7 @@ export const inventoryController: RequestHandler = async (request, response) => cleanupInventory(inventory); inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); - await inventory.save(); + //await inventory.save(); } if ( @@ -96,9 +98,20 @@ export const inventoryController: RequestHandler = async (request, response) => new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown ) { handleSubsumeCompletion(inventory); - await inventory.save(); + //await inventory.save(); } + if (inventory.LastInventorySync) { + const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000); + const currentDuviriMood = Math.trunc(Date.now() / 7200000); + if (lastSyncDuviriMood != currentDuviriMood) { + logger.debug(`refreshing duviri seed`); + inventory.DuviriInfo.Seed = generateRewardSeed(); + } + } + inventory.LastInventorySync = new Types.ObjectId(); + await inventory.save(); + response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query)); }; @@ -274,7 +287,7 @@ export const getInventoryResponse = async ( } // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. - //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId()); + inventoryResponse.LastInventorySync = undefined; // Set 2FA enabled so trading post can be used inventoryResponse.HWIDProtectEnabled = true; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 2c1b4b09..dcb5a536 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1662,6 +1662,7 @@ const inventorySchema = new Schema( //Unknown and system DuviriInfo: DuviriInfoSchema, + LastInventorySync: Schema.Types.ObjectId, Mailbox: MailboxSchema, HandlerPoints: Number, ChallengesFixVersion: { type: Number, default: 6 }, @@ -1759,6 +1760,9 @@ inventorySchema.set("toJSON", { sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined }; } + if (inventoryDatabase.LastInventorySync) { + inventoryResponse.LastInventorySync = toOid(inventoryDatabase.LastInventorySync); + } } }); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 3662dc23..4973ff87 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -52,6 +52,7 @@ export interface IInventoryDatabase | "LastLiteSortieReward" | "CrewMembers" | "QualifyingInvasions" + | "LastInventorySync" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -89,6 +90,7 @@ export interface IInventoryDatabase LastLiteSortieReward?: ILastSortieRewardDatabase[]; CrewMembers: ICrewMemberDatabase[]; QualifyingInvasions: IInvasionProgressDatabase[]; + LastInventorySync?: Types.ObjectId; } export interface IQuestKeyDatabase { @@ -333,7 +335,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu LotusCustomization?: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; - LastInventorySync: IOid; + LastInventorySync?: IOid; NextRefill?: IMongoDate; FoundToday?: IMiscItem[]; // for Argon Crystals CustomMarkers?: ICustomMarkers[]; From 9042e8535526abc96e1dfd46c1c5ca3b44c597f9 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:00:38 -0700 Subject: [PATCH 649/776] feat: infested lich rewards (#1898) Closes #1884 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1898 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 | 3 +- src/helpers/nemesisHelpers.ts | 114 +++++++++++++++++- src/services/missionInventoryUpdateService.ts | 12 +- src/services/rngService.ts | 5 +- 4 files changed, 127 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 4c3b8830..7adc2d3d 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -13,6 +13,7 @@ import { addItems, combineInventoryChanges, getInventory } from "@/src/services/ import { logger } from "@/src/utils/logger"; import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; export const inboxController: RequestHandler = async (req, res) => { const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; @@ -48,7 +49,7 @@ export const inboxController: RequestHandler = async (req, res) => { await addItems( inventory, attachmentItems.map(attItem => ({ - ItemType: attItem, + ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem, ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1 })), inventoryChanges diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 17a2d3f5..8959d985 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -1,12 +1,14 @@ import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; -import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; -import { SRng } from "@/src/services/rngService"; +import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { getRewardAtPercentage, SRng } from "@/src/services/rngService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { logger } from "../utils/logger"; import { IOid } from "../types/commonTypes"; import { Types } from "mongoose"; -import { addMods } from "../services/inventoryService"; +import { addMods, generateRewardSeed } from "../services/inventoryService"; import { isArchwingMission } from "../services/worldStateService"; +import { fromStoreItem, toStoreItem } from "../services/itemDataService"; +import { createMessage } from "../services/inboxService"; export const getInfNodes = (faction: string, rank: number): IInfNode[] => { const infNodes = []; @@ -340,3 +342,109 @@ export const getInnateDamageValue = (fp: bigint): number => { } return Math.trunc(value * 0x40000000); }; + +export const getKillTokenRewardCount = (fp: bigint): number => { + const rng = new SRng(fp); + return rng.randomInt(10, 15); +}; + +// /Lotus/Types/Enemies/InfestedLich/InfestedLichRewardManifest +const infestedLichRotA = [ + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterA", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterB", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandDespairPoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandGridPoster", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandHuddlePoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandJumpPoster", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLimoPoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterDay", probability: 0.046 }, + { + type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterNight", + probability: 0.045 + }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandSillyPoster", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhiteBluePoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhitePinkPoster", probability: 0.045 } +]; +const infestedLichRotB = [ + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraA", probability: 0.072 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraB", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraC", probability: 0.072 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraD", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraE", probability: 0.072 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraF", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraG", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraH", probability: 0.072 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDJRomHype", probability: 0.071 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DancePacketWindmillShuffle", probability: 0.072 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceHarddrivePony", probability: 0.071 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDrillbitCrisscross", probability: 0.072 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceZekeCanthavethis", probability: 0.071 }, + { type: "/Lotus/StoreItems/Types/Items/PhotoBooth/PhotoboothTileRJLasXStadiumBossArena", probability: 0.071 } +]; +export const getInfestedLichItemRewards = (fp: bigint): string[] => { + const rng = new SRng(fp); + const rotAReward = getRewardAtPercentage(infestedLichRotA, rng.randomFloat())!.type; + rng.randomFloat(); // unused afaict + const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type; + return [rotAReward, rotBReward]; +}; + +export const sendCodaFinishedMessage = async ( + inventory: TInventoryDatabaseDocument, + fp: bigint = generateRewardSeed(), + name: string = "ZEKE_BEATWOMAN_TM.1999", + killed: boolean = true +): Promise => { + const att: string[] = []; + + // First vanquish/convert gives a sigil + const sigil = killed + ? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil" + : "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil"; + if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) { + att.push(toStoreItem(sigil)); + } + + const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp); + att.push(fromStoreItem(rotAReward)); + att.push(fromStoreItem(rotBReward)); + + let countedAtt: ITypeCount[] | undefined; + if (killed) { + countedAtt = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + ItemCount: getKillTokenRewardCount(fp) + } + ]; + } + + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/VanquishBandMsgBody", + arg: [ + { + Key: "LICH_NAME", + Tag: name + } + ], + att: att, + countedAtt: countedAtt, + sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + highPriority: true + } + ]); +}; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 3ab2d680..89b55cea 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -55,7 +55,7 @@ import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent. import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; -import { getInfNodes, getWeaponsForManifest } from "@/src/helpers/nemesisHelpers"; +import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; @@ -639,7 +639,10 @@ export const addMissionInventoryUpdates = async ( }); if (value.killed) { - if (value.weaponLoc) { + if ( + value.weaponLoc && + inventory.Nemesis.Faction != "FC_INFESTATION" // weaponLoc is "/Lotus/Language/Weapons/DerelictCernosName" for these for some reason + ) { const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[ inventory.Nemesis.WeaponIdx ]; @@ -657,6 +660,11 @@ export const addMissionInventoryUpdates = async ( } } + // TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given? + if (inventory.Nemesis.Faction == "FC_INFESTATION") { + await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed); + } + inventory.Nemesis = undefined; } break; diff --git a/src/services/rngService.ts b/src/services/rngService.ts index bb9028b2..84a4ed8d 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -18,7 +18,10 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -const getRewardAtPercentage = (pool: T[], percentage: number): T | undefined => { +export const getRewardAtPercentage = ( + pool: T[], + percentage: number +): T | undefined => { if (pool.length == 0) return; const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); From a1872e2b0721b49936e473d0d2b31cca5a8c8826 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:00:51 -0700 Subject: [PATCH 650/776] chore: simplify getInnateDamageTag (#1899) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1899 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++-- package.json | 2 +- src/helpers/nemesisHelpers.ts | 74 +---------------------------------- 3 files changed, 6 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45cca760..beab4607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.58", + "warframe-public-export-plus": "^0.5.59", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.58", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.58.tgz", - "integrity": "sha512-2G3tKcoblUl7S3Rkk5k/qH+VGZBUmU2QjtIrEO/Bt6UlgO83s648elkNdDKOLBKXnxIsa194nVwz+ci1K86sXg==" + "version": "0.5.59", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.59.tgz", + "integrity": "sha512-/SUCVjngVDBz6gahz7CdVLywtHLODL6O5nmNtQcxFDUwrUGnF1lETcG8/UO+WLeGxBVAy4BDPbq+9ZWlYZM4uQ==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 816118ff..e5deb9bc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.58", + "warframe-public-export-plus": "^0.5.59", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 8959d985..3a146831 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -246,7 +246,6 @@ export const getWeaponsForManifest = (manifest: string): readonly string[] => { throw new Error(`unknown nemesis manifest: ${manifest}`); }; -// TODO: This sucks. export const getInnateDamageTag = ( KillingSuit: string ): @@ -257,78 +256,7 @@ export const getInnateDamageTag = ( | "InnateMagDamage" | "InnateRadDamage" | "InnateToxinDamage" => { - const baseSuitType = ExportWarframes[KillingSuit].parentName; - switch (baseSuitType) { - case "/Lotus/Powersuits/Volt/VoltBaseSuit": - case "/Lotus/Powersuits/Excalibur/ExcaliburBaseSuit": - case "/Lotus/Powersuits/AntiMatter/NovaBaseSuit": - case "/Lotus/Powersuits/Banshee/BansheeBaseSuit": - case "/Lotus/Powersuits/Berserker/BerserkerBaseSuit": - case "/Lotus/Powersuits/Magician/MagicianBaseSuit": - case "/Lotus/Powersuits/Sentient/SentientBaseSuit": - case "/Lotus/Powersuits/Gyre/GyreBaseSuit": - return "InnateElectricityDamage"; - case "/Lotus/Powersuits/Ember/EmberBaseSuit": - case "/Lotus/Powersuits/Dragon/DragonBaseSuit": - case "/Lotus/Powersuits/Nezha/NezhaBaseSuit": - case "/Lotus/Powersuits/Sandman/SandmanBaseSuit": - case "/Lotus/Powersuits/Trapper/TrapperBaseSuit": - case "/Lotus/Powersuits/Wisp/WispBaseSuit": - case "/Lotus/Powersuits/Odalisk/OdaliskBaseSuit": - case "/Lotus/Powersuits/PaxDuviricus/PaxDuviricusBaseSuit": - case "/Lotus/Powersuits/Choir/ChoirBaseSuit": - case "/Lotus/Powersuits/Temple/TempleBaseSuit": - return "InnateHeatDamage"; - case "/Lotus/Powersuits/Frost/FrostBaseSuit": - case "/Lotus/Powersuits/Glass/GlassBaseSuit": - case "/Lotus/Powersuits/Fairy/FairyBaseSuit": - case "/Lotus/Powersuits/IronFrame/IronFrameBaseSuit": - case "/Lotus/Powersuits/Revenant/RevenantBaseSuit": - case "/Lotus/Powersuits/Trinity/TrinityBaseSuit": - case "/Lotus/Powersuits/Hoplite/HopliteBaseSuit": - case "/Lotus/Powersuits/Koumei/KoumeiBaseSuit": - return "InnateFreezeDamage"; - case "/Lotus/Powersuits/Saryn/SarynBaseSuit": - case "/Lotus/Powersuits/Paladin/PaladinBaseSuit": - case "/Lotus/Powersuits/Brawler/BrawlerBaseSuit": - case "/Lotus/Powersuits/Infestation/InfestationBaseSuit": - case "/Lotus/Powersuits/Necro/NecroBaseSuit": - case "/Lotus/Powersuits/Khora/KhoraBaseSuit": - case "/Lotus/Powersuits/Ranger/RangerBaseSuit": - case "/Lotus/Powersuits/Dagath/DagathBaseSuit": - return "InnateToxinDamage"; - case "/Lotus/Powersuits/Mag/MagBaseSuit": - case "/Lotus/Powersuits/Pirate/PirateBaseSuit": - case "/Lotus/Powersuits/Cowgirl/CowgirlBaseSuit": - case "/Lotus/Powersuits/Priest/PriestBaseSuit": - case "/Lotus/Powersuits/BrokenFrame/BrokenFrameBaseSuit": - case "/Lotus/Powersuits/Alchemist/AlchemistBaseSuit": - case "/Lotus/Powersuits/Yareli/YareliBaseSuit": - case "/Lotus/Powersuits/Geode/GeodeBaseSuit": - case "/Lotus/Powersuits/Frumentarius/FrumentariusBaseSuit": - return "InnateMagDamage"; - case "/Lotus/Powersuits/Loki/LokiBaseSuit": - case "/Lotus/Powersuits/Ninja/NinjaBaseSuit": - case "/Lotus/Powersuits/Jade/JadeBaseSuit": - case "/Lotus/Powersuits/Bard/BardBaseSuit": - case "/Lotus/Powersuits/Harlequin/HarlequinBaseSuit": - case "/Lotus/Powersuits/Garuda/GarudaBaseSuit": - case "/Lotus/Powersuits/YinYang/YinYangBaseSuit": - case "/Lotus/Powersuits/Werewolf/WerewolfBaseSuit": - case "/Lotus/Powersuits/ConcreteFrame/ConcreteFrameBaseSuit": - return "InnateRadDamage"; - case "/Lotus/Powersuits/Rhino/RhinoBaseSuit": - case "/Lotus/Powersuits/Tengu/TenguBaseSuit": - case "/Lotus/Powersuits/MonkeyKing/MonkeyKingBaseSuit": - case "/Lotus/Powersuits/Runner/RunnerBaseSuit": - case "/Lotus/Powersuits/Pacifist/PacifistBaseSuit": - case "/Lotus/Powersuits/Devourer/DevourerBaseSuit": - case "/Lotus/Powersuits/Wraith/WraithBaseSuit": - case "/Lotus/Powersuits/Pagemaster/PagemasterBaseSuit": - return "InnateImpactDamage"; - } - logger.warn(`unknown innate damage type for ${KillingSuit}, using heat as a fallback`); - return "InnateHeatDamage"; + return ExportWarframes[KillingSuit].nemesisUpgradeTag!; }; // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003. From 9417aa3c8462e43950a0b0c853197ad34ba95e7d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:01:02 -0700 Subject: [PATCH 651/776] fix: only consider market-listed blueprints for login reward (#1900) Closes #1882 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1900 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/loginRewardService.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index 2f42f04b..c897442e 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -77,7 +77,6 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab const reward = rng.randomReward(randomRewards)!; //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; if (reward.RewardType == "RT_RANDOM_RECIPE") { - // Not very faithful implementation but roughly the same idea const masteredItems = new Set(); for (const entry of inventory.XPInfo) { masteredItems.add(entry.ItemType); @@ -95,12 +94,12 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab } const eligibleRecipes: string[] = []; for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) { - if (unmasteredItems.has(recipe.resultType)) { + if (!recipe.excludeFromMarket && unmasteredItems.has(recipe.resultType)) { eligibleRecipes.push(uniqueName); } } if (eligibleRecipes.length == 0) { - // This account has all warframes and weapons already mastered (filthy cheater), need a different reward. + // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward. return getRandomLoginReward(rng, day, inventory); } reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)); From 5df533a7fb75f3b7167f6428d857570540868997 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:01:17 -0700 Subject: [PATCH 652/776] chore: auto-generate "daily special" for fish vendors (#1902) Trying to go a bit more towards an "auto-generate by default" approach, with manual overrides where needed. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1902 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 47 +++++--- .../DeimosFishmongerVendorManifest.json | 106 ------------------ .../OstronFishmongerVendorManifest.json | 106 ------------------ .../SolarisFishmongerVendorManifest.json | 106 ------------------ 4 files changed, 33 insertions(+), 332 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json delete mode 100644 static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json delete mode 100644 static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 1ca1fa2a..d594e79f 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,4 +1,5 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; @@ -6,7 +7,6 @@ import { ExportVendors, IRange } from "warframe-public-export-plus"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; -import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json"; import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json"; import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json"; import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json"; @@ -22,12 +22,10 @@ import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorI import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; -import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; -import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json"; import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; @@ -36,7 +34,6 @@ import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVe const rawVendorManifests: IVendorManifest[] = [ ArchimedeanVendorManifest, DeimosEntratiFragmentVendorProductsManifest, - DeimosFishmongerVendorManifest, DeimosHivemindCommisionsManifestFishmonger, DeimosHivemindCommisionsManifestPetVendor, DeimosHivemindCommisionsManifestProspector, @@ -52,12 +49,10 @@ const rawVendorManifests: IVendorManifest[] = [ HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, Nova1999ConquestShopManifest, - OstronFishmongerVendorManifest, OstronPetVendorManifest, OstronProspectorVendorManifest, RadioLegionIntermission12VendorManifest, SolarisDebtTokenVendorRepossessionsManifest, - SolarisFishmongerVendorManifest, SolarisProspectorVendorManifest, Temple1999VendorManifest, TeshinHardModeVendorManifest, // uses preprocessing @@ -87,17 +82,11 @@ const generatableVendors: IGeneratableVendorInfo[] = [ cycleOffset: 1744934400_000, cycleDuration: 4 * unixTimesInMs.day }, - { - _id: { $oid: "5be4a159b144f3cdf1c22efa" }, - TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", - RandomSeedType: "VRST_FLAVOUR_TEXT", - cycleDuration: unixTimesInMs.hour - }, { _id: { $oid: "61ba123467e5d37975aeeb03" }, TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", RandomSeedType: "VRST_FLAVOUR_TEXT", - cycleDuration: unixTimesInMs.week + cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly. } // { // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, @@ -105,6 +94,10 @@ const generatableVendors: IGeneratableVendorInfo[] = [ // } ]; +const getVendorOid = (typeName: string): string => { + return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0"); +}; + export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { @@ -116,6 +109,14 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | return generateVendorManifest(vendorInfo); } } + if (typeName in ExportVendors) { + return generateVendorManifest({ + _id: { $oid: getVendorOid(typeName) }, + TypeName: typeName, + RandomSeedType: ExportVendors[typeName].randomSeedType, + cycleDuration: unixTimesInMs.hour + }); + } return undefined; }; @@ -130,6 +131,17 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined return generateVendorManifest(vendorInfo); } } + for (const [typeName, manifest] of Object.entries(ExportVendors)) { + const typeNameOid = getVendorOid(typeName); + if (typeNameOid == oid) { + return generateVendorManifest({ + _id: { $oid: typeNameOid }, + TypeName: typeName, + RandomSeedType: manifest.randomSeedType, + cycleDuration: unixTimesInMs.hour + }); + } + } return undefined; }; @@ -195,7 +207,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const rng = new CRng(mixSeeds(vendorSeed, cycleIndex)); const manifest = ExportVendors[vendorInfo.TypeName]; const offersToAdd = []; - if (manifest.numItems && manifest.numItems.minValue != manifest.numItems.maxValue) { + if (manifest.numItems && !manifest.isOneBinPerCycle) { const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) { // TODO: Consider per-bin item limits @@ -263,6 +275,13 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani ) * rawItem.credits.step; item.RegularPrice = [value, value]; } + if (rawItem.platinum) { + const value = + typeof rawItem.platinum == "number" + ? rawItem.platinum + : rng.randomInt(rawItem.platinum.minValue, rawItem.platinum.maxValue); + item.PremiumPrice = [value, value]; + } if (vendorInfo.RandomSeedType) { item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); if (vendorInfo.RandomSeedType == "VRST_WEAPON") { diff --git a/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json b/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json deleted file mode 100644 index ac6c0951..00000000 --- a/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5f456e01c96976e97d6b8016" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Deimos/FishmongerVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosOrokinFishAPartItem", - "PremiumPrice": [9, 9], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91b9" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishDPartItem", - "PremiumPrice": [17, 17], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91ba" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishCPartItem", - "PremiumPrice": [10, 10], - "Bin": "BIN_1", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91bb" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishBPartItem", - "PremiumPrice": [6, 6], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91bc" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishAPartItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91bd" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosGenericSharedFishPartItem", - "PremiumPrice": [7, 7], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91be" - } - } - ], - "PropertyTextHash": "6DF13A7FB573C25B4B4F989CBEFFC615", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} diff --git a/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json b/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json deleted file mode 100644 index c6a3670b..00000000 --- a/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "59d6e27ebcc718474eb17115" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Ostron/FishmongerVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayUncommonFishAPartItem", - "PremiumPrice": [14, 14], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9808" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/BothUncommonFishBPartItem", - "PremiumPrice": [12, 12], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9809" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishCPartItem", - "PremiumPrice": [8, 8], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishBPartItem", - "PremiumPrice": [7, 7], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishAPartItem", - "PremiumPrice": [10, 10], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980c" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/BothCommonFishBPartItem", - "PremiumPrice": [8, 8], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980d" - } - } - ], - "PropertyTextHash": "CC3B9DAFB38F412998E90A41421A8986", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} diff --git a/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json b/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json deleted file mode 100644 index 4a4dcb64..00000000 --- a/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5b0de8556df82a56ea9bae82" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/FishmongerVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem", - "PremiumPrice": [15, 15], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9515" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishVenedoCaseItem", - "PremiumPrice": [8, 8], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9516" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/SolarisFishDissipatorCoilItem", - "PremiumPrice": [18, 18], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9517" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishExaBrainItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9518" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9519" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/GenericFishScrapItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e951a" - } - } - ], - "PropertyTextHash": "946131D0CF5CDF7C2C03BB967DE0DF49", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} From 743b7847540ea796ae20ccd1d83bf27cb5a089fd Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Mon, 28 Apr 2025 14:01:35 -0700 Subject: [PATCH 653/776] chore(webui): use plural form of "Moa", just to stay consistent with the other categories (#1905) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1905 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- static/webui/translations/en.js | 2 +- static/webui/translations/es.js | 2 +- static/webui/translations/fr.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 40b93ecb..59c906c7 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -84,7 +84,7 @@ dict = { inventory_sentinelWeapons: `Wächter-Waffen`, inventory_operatorAmps: `Verstärker`, inventory_hoverboards: `K-Drives`, - inventory_moaPets: `Moa`, + inventory_moaPets: `Moas`, inventory_kubrowPets: `Bestien`, inventory_evolutionProgress: `Incarnon-Entwicklungsfortschritte`, inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 01a4a23f..4e7af430 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -83,7 +83,7 @@ dict = { inventory_sentinelWeapons: `Sentinel Weapons`, inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, - inventory_moaPets: `Moa`, + inventory_moaPets: `Moas`, inventory_kubrowPets: `Beasts`, inventory_evolutionProgress: `Incarnon Evolution Progress`, inventory_bulkAddSuits: `Add Missing Warframes`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index dd4cd9ab..5660fafd 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -84,7 +84,7 @@ dict = { inventory_sentinelWeapons: `Armas de centinela`, inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, - inventory_moaPets: `Moa`, + inventory_moaPets: `Moas`, inventory_kubrowPets: `Bestias`, inventory_evolutionProgress: `Progreso de evolución Incarnon`, inventory_bulkAddSuits: `Agregar Warframes faltantes`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 1dab80e6..afaf94bf 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -84,7 +84,7 @@ dict = { inventory_sentinelWeapons: `Armes de sentinelles`, inventory_operatorAmps: `Amplificateurs`, inventory_hoverboards: `K-Drives`, - inventory_moaPets: `Moa`, + inventory_moaPets: `Moas`, inventory_kubrowPets: `Bêtes`, inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, From ab9cc685eb6ffef2cdef527a6999df0c796dec6e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 29 Apr 2025 02:05:18 -0700 Subject: [PATCH 654/776] fix: exclude capture as a mission type for sorties (#1909) Closes #1865 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1909 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index cc93b48a..6fd0b71a 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -269,7 +269,10 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && key in sortieTilesets ) { - if (!availableMissionIndexes.includes(value.missionIndex)) { + if ( + value.missionIndex != 5 && // Sorties do not have capture missions + !availableMissionIndexes.includes(value.missionIndex) + ) { availableMissionIndexes.push(value.missionIndex); } nodes.push(key); From 1cf7b41d3f3072b631d1870f5f1b2c967762a490 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:27:25 -0700 Subject: [PATCH 655/776] chore: note that random element functions could return undefined (#1910) We should be explicit about the fact that we expect the arrays to not be empty. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1910 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/activateRandomModController.ts | 2 +- .../api/artifactTransmutationController.ts | 2 +- .../api/modularWeaponSaleController.ts | 2 +- src/helpers/rivenHelper.ts | 8 ++++---- src/services/inventoryService.ts | 10 +++++----- src/services/loginRewardService.ts | 2 +- src/services/missionInventoryUpdateService.ts | 2 +- src/services/personalRoomsService.ts | 12 +++++------ src/services/rngService.ts | 6 +++--- src/services/serversideVendorsService.ts | 4 ++-- src/services/worldStateService.ts | 20 +++++++++---------- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/controllers/api/activateRandomModController.ts b/src/controllers/api/activateRandomModController.ts index 4ac822c7..5dc8499c 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -17,7 +17,7 @@ export const activateRandomModController: RequestHandler = async (req, res) => { ItemCount: -1 } ]); - const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]); + const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!; const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const upgradeIndex = inventory.Upgrades.push({ diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts index 7e39e79e..4310c7c4 100644 --- a/src/controllers/api/artifactTransmutationController.ts +++ b/src/controllers/api/artifactTransmutationController.ts @@ -28,7 +28,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res) }); const rawRivenType = getRandomRawRivenType(); - const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]); + const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!; const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const upgradeIndex = diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index f60f6c3d..3e37547b 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -141,7 +141,7 @@ const getModularWeaponSale = ( getItemType: (parts: string[]) => string ): IModularWeaponSaleInfo => { const rng = new CRng(day); - const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])); + const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!); let partsCost = 0; for (const part of parts) { partsCost += ExportWeapons[part].premiumPrice!; diff --git a/src/helpers/rivenHelper.ts b/src/helpers/rivenHelper.ts index 35426a29..34a7babc 100644 --- a/src/helpers/rivenHelper.ts +++ b/src/helpers/rivenHelper.ts @@ -31,7 +31,7 @@ export interface IFingerprintStat { } export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFingerprint => { - const challenge = getRandomElement(meta.availableChallenges!); + const challenge = getRandomElement(meta.availableChallenges!)!; const fingerprintChallenge: IRivenChallenge = { Type: challenge.fullName, Progress: 0, @@ -54,11 +54,11 @@ export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFinger export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => { const fingerprint: IUnveiledRivenFingerprint = { - compat: getRandomElement(meta.compatibleItems!), + compat: getRandomElement(meta.compatibleItems!)!, lim: 0, lvl: 0, lvlReq: getRandomInt(8, 16), - pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), + pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"])!, buffs: [], curses: [] }; @@ -81,7 +81,7 @@ export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenF if (Math.random() < 0.5) { const entry = getRandomElement( meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) - ); + )!; fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); } }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index c3d47ec6..da8b0848 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1798,7 +1798,7 @@ export const addKeyChainItems = async ( }; export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => { - const enemyTypes = getRandomElement(libraryDailyTasks); + const enemyTypes = getRandomElement(libraryDailyTasks)!; const enemyAvatar = ExportEnemies.avatars[enemyTypes[0]]; const scansRequired = getRandomInt(2, 4); return { @@ -1944,22 +1944,22 @@ export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, neme "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" - ]); + ])!; const body = getRandomElement([ "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC" - ]); + ])!; const legs = getRandomElement([ "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC" - ]); + ])!; const tail = getRandomElement([ "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC" - ]); + ])!; const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0]; addRecipes(inventory, [ { diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index c897442e..1734247a 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -102,7 +102,7 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward. return getRandomLoginReward(rng, day, inventory); } - reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)); + reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!); } return { //_id: toOid(new Types.ObjectId()), diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 89b55cea..35486d21 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -930,7 +930,7 @@ export const addMissionRewards = async ( if (rewardInfo.useVaultManifest) { MissionRewards.push({ - StoreItem: getRandomElement(corruptedMods), + StoreItem: getRandomElement(corruptedMods)!, ItemCount: 1 }); } diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 97f1b41d..8cf49ec1 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -47,12 +47,12 @@ export const createGarden = (): IGardeningDatabase => { Name: "Garden0", Plants: [ { - PlantType: getRandomElement(plantTypes), + PlantType: getRandomElement(plantTypes)!, EndTime: endTime, PlotIndex: 0 }, { - PlantType: getRandomElement(plantTypes), + PlantType: getRandomElement(plantTypes)!, EndTime: endTime, PlotIndex: 1 } @@ -62,12 +62,12 @@ export const createGarden = (): IGardeningDatabase => { Name: "Garden1", Plants: [ { - PlantType: getRandomElement(plantTypes), + PlantType: getRandomElement(plantTypes)!, EndTime: endTime, PlotIndex: 0 }, { - PlantType: getRandomElement(plantTypes), + PlantType: getRandomElement(plantTypes)!, EndTime: endTime, PlotIndex: 1 } @@ -77,12 +77,12 @@ export const createGarden = (): IGardeningDatabase => { Name: "Garden2", Plants: [ { - PlantType: getRandomElement(plantTypes), + PlantType: getRandomElement(plantTypes)!, EndTime: endTime, PlotIndex: 0 }, { - PlantType: getRandomElement(plantTypes), + PlantType: getRandomElement(plantTypes)!, EndTime: endTime, PlotIndex: 1 } diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 84a4ed8d..68225ca1 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -6,7 +6,7 @@ export interface IRngResult { probability: number; } -export const getRandomElement = (arr: T[]): T => { +export const getRandomElement = (arr: T[]): T | undefined => { return arr[Math.floor(Math.random() * arr.length)]; }; @@ -113,7 +113,7 @@ export class CRng { return min; } - randomElement(arr: T[]): T { + randomElement(arr: T[]): T | undefined { return arr[Math.floor(this.random() * arr.length)]; } @@ -145,7 +145,7 @@ export class SRng { return min; } - randomElement(arr: T[]): T { + randomElement(arr: T[]): T | undefined { return arr[this.randomInt(0, arr.length - 1)]; } diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index d594e79f..82ceae46 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -212,7 +212,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) { // TODO: Consider per-bin item limits // TODO: Consider item probability weightings - offersToAdd.push(rng.randomElement(manifest.items)); + offersToAdd.push(rng.randomElement(manifest.items)!); } } else { let binThisCycle; @@ -256,7 +256,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani for (let i = 0; i != rawItem.numRandomItemPrices; ++i) { let itemPrice: { type: string; count: IRange }; do { - itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin]); + itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin])!; } while (item.ItemPrices.find(x => x.ItemType == itemPrice.type)); item.ItemPrices.push({ ItemType: itemPrice.type, diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 6fd0b71a..e2e2ea23 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -225,7 +225,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { const seed = new CRng(day).randomInt(0, 0xffff); const rng = new CRng(seed); - const boss = rng.randomElement(sortieBosses); + const boss = rng.randomElement(sortieBosses)!; const modifiers = [ "SORTIE_MODIFIER_LOW_ENERGY", @@ -302,7 +302,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { missionIndex = ExportRegions[node].missionIndex; if (missionIndex != 28) { - missionIndex = rng.randomElement(availableMissionIndexes); + missionIndex = rng.randomElement(availableMissionIndexes)!; } } while ( specialMissionTypes.indexOf(missionIndex) != -1 && @@ -312,7 +312,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { if (i == 2 && rng.randomInt(0, 2) == 2) { const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); - const modifierType = rng.randomElement(filteredModifiers); + const modifierType = rng.randomElement(filteredModifiers)!; if (boss == "SORTIE_BOSS_PHORID") { selectedNodes.push({ @@ -346,7 +346,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") : modifiers; - const modifierType = rng.randomElement(filteredModifiers); + const modifierType = rng.randomElement(filteredModifiers)!; selectedNodes.push({ missionType, @@ -389,7 +389,7 @@ const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { Daily: true, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Challenge: rng.randomElement(dailyChallenges) + Challenge: rng.randomElement(dailyChallenges)! }; }; @@ -408,7 +408,7 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: rng.randomElement(weeklyChallenges) + Challenge: rng.randomElement(weeklyChallenges)! }; }; @@ -425,7 +425,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: rng.randomElement(weeklyHardChallenges) + Challenge: rng.randomElement(weeklyHardChallenges)! }; }; @@ -1196,7 +1196,7 @@ export const getLiteSortie = (week: number): ILiteSortie => { "MT_EXTERMINATION", "MT_SABOTAGE", "MT_RESCUE" - ]), + ])!, node: firstNode }, { @@ -1206,8 +1206,8 @@ export const getLiteSortie = (week: number): ILiteSortie => { "MT_ARTIFACT", "MT_EXCAVATE", "MT_SURVIVAL" - ]), - node: rng.randomElement(nodes) + ])!, + node: rng.randomElement(nodes)! }, { missionType: "MT_ASSASSINATION", From de1e2a25f239624b0b72b5eb63a19b7a1c18804c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:27:38 -0700 Subject: [PATCH 656/776] fix(webui): ensure that all requests using authz revalidate it (#1911) Closes #1907 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1911 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 281 +++++++++++++++++++++-------------------- 1 file changed, 147 insertions(+), 134 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index b7c63fdf..6c18ed52 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -375,6 +375,7 @@ function fetchItemList() { } fetchItemList(); +// Assumes that caller revalidates authz function updateInventory() { const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); req.done(data => { @@ -487,25 +488,27 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); - if (item.XP < maxXP) { - addGearExp(category, item.ItemId.$oid, maxXP - item.XP); - } - if ("exalted" in itemMap[item.ItemType]) { - for (const exaltedType of itemMap[item.ItemType].exalted) { - const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType); - if (exaltedItem) { - const exaltedCap = - itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; - if (exaltedItem.XP < exaltedCap) { - addGearExp( - "SpecialItems", - exaltedItem.ItemId.$oid, - exaltedCap - exaltedItem.XP - ); + revalidateAuthz(() => { + if (item.XP < maxXP) { + addGearExp(category, item.ItemId.$oid, maxXP - item.XP); + } + if ("exalted" in itemMap[item.ItemType]) { + for (const exaltedType of itemMap[item.ItemType].exalted) { + const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType); + if (exaltedItem) { + const exaltedCap = + itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; + if (exaltedItem.XP < exaltedCap) { + addGearExp( + "SpecialItems", + exaltedItem.ItemId.$oid, + exaltedCap - exaltedItem.XP + ); + } } } } - } + }); }; a.title = loc("code_maxRank"); a.innerHTML = ``; @@ -1229,76 +1232,22 @@ function addMissingEvolutionProgress() { } function maxRankAllEvolutions() { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + revalidateAuthz(() => { + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + req.done(data => { + const requests = []; - req.done(data => { - const requests = []; - - data.EvolutionProgress.forEach(item => { - if (item.Rank < 5) { - requests.push({ - ItemType: item.ItemType, - Rank: 5 - }); - } - }); - - if (Object.keys(requests).length > 0) { - return setEvolutionProgress(requests); - } - - toast(loc("code_noEquipmentToRankUp")); - }); -} - -function maxRankAllEquipment(categories) { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); - - req.done(data => { - window.itemListPromise.then(itemMap => { - const batchData = {}; - - categories.forEach(category => { - data[category].forEach(item => { - const maxXP = - category === "Suits" || - category === "SpaceSuits" || - category === "Sentinels" || - category === "Hoverboards" - ? 1_600_000 - : 800_000; - - if (item.XP < maxXP) { - if (!batchData[category]) { - batchData[category] = []; - } - batchData[category].push({ - ItemId: { $oid: item.ItemId.$oid }, - XP: maxXP - }); - } - if (category === "Suits") { - if ("exalted" in itemMap[item.ItemType]) { - for (const exaltedType of itemMap[item.ItemType].exalted) { - const exaltedItem = data["SpecialItems"].find(x => x.ItemType == exaltedType); - if (exaltedItem) { - const exaltedCap = itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; - if (exaltedItem.XP < exaltedCap) { - batchData["SpecialItems"] ??= []; - batchData["SpecialItems"].push({ - ItemId: { $oid: exaltedItem.ItemId.$oid }, - XP: exaltedCap - }); - } - } - } - } - } - }); + data.EvolutionProgress.forEach(item => { + if (item.Rank < 5) { + requests.push({ + ItemType: item.ItemType, + Rank: 5 + }); + } }); - if (Object.keys(batchData).length > 0) { - return sendBatchGearExp(batchData); + if (Object.keys(requests).length > 0) { + return setEvolutionProgress(requests); } toast(loc("code_noEquipmentToRankUp")); @@ -1306,6 +1255,64 @@ function maxRankAllEquipment(categories) { }); } +function maxRankAllEquipment(categories) { + revalidateAuthz(() => { + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + req.done(data => { + window.itemListPromise.then(itemMap => { + const batchData = {}; + + categories.forEach(category => { + data[category].forEach(item => { + const maxXP = + category === "Suits" || + category === "SpaceSuits" || + category === "Sentinels" || + category === "Hoverboards" + ? 1_600_000 + : 800_000; + + if (item.XP < maxXP) { + if (!batchData[category]) { + batchData[category] = []; + } + batchData[category].push({ + ItemId: { $oid: item.ItemId.$oid }, + XP: maxXP + }); + } + if (category === "Suits") { + if ("exalted" in itemMap[item.ItemType]) { + for (const exaltedType of itemMap[item.ItemType].exalted) { + const exaltedItem = data["SpecialItems"].find(x => x.ItemType == exaltedType); + if (exaltedItem) { + const exaltedCap = + itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; + if (exaltedItem.XP < exaltedCap) { + batchData["SpecialItems"] ??= []; + batchData["SpecialItems"].push({ + ItemId: { $oid: exaltedItem.ItemId.$oid }, + XP: exaltedCap + }); + } + } + } + } + } + }); + }); + + if (Object.keys(batchData).length > 0) { + return sendBatchGearExp(batchData); + } + + toast(loc("code_noEquipmentToRankUp")); + }); + }); + }); +} + +// Assumes that caller revalidates authz function addGearExp(category, oid, xp) { const data = {}; data[category] = [ @@ -1314,16 +1321,14 @@ function addGearExp(category, oid, xp) { XP: xp } ]; - revalidateAuthz(() => { - $.post({ - url: "/custom/addXp?" + window.authz, - contentType: "application/json", - data: JSON.stringify(data) - }).done(function () { - if (category != "SpecialItems") { - updateInventory(); - } - }); + $.post({ + url: "/custom/addXp?" + window.authz, + contentType: "application/json", + data: JSON.stringify(data) + }).done(function () { + if (category != "SpecialItems") { + updateInventory(); + } }); } @@ -1598,32 +1603,34 @@ function doAcquireMod() { const uiConfigs = [...$("#server-settings input[id]")].map(x => x.id); function doChangeSettings() { - fetch("/custom/config?" + window.authz) - .then(response => response.json()) - .then(json => { - for (const i of uiConfigs) { - var x = document.getElementById(i); - if (x != null) { - if (x.type == "checkbox") { - if (x.checked === true) { - json[i] = true; - } else { - json[i] = false; + revalidateAuthz(() => { + fetch("/custom/config?" + window.authz) + .then(response => response.json()) + .then(json => { + for (const i of uiConfigs) { + var x = document.getElementById(i); + if (x != null) { + if (x.type == "checkbox") { + if (x.checked === true) { + json[i] = true; + } else { + json[i] = false; + } + } else if (x.type == "number") { + json[i] = parseInt(x.value); } - } else if (x.type == "number") { - json[i] = parseInt(x.value); } } - } - $.post({ - url: "/custom/config?" + window.authz, - contentType: "text/plain", - data: JSON.stringify(json, null, 2) - }).then(() => { - // A few cheats affect the inventory response which in turn may change what values we need to show - updateInventory(); + $.post({ + url: "/custom/config?" + window.authz, + contentType: "text/plain", + data: JSON.stringify(json, null, 2) + }).then(() => { + // A few cheats affect the inventory response which in turn may change what values we need to show + updateInventory(); + }); }); - }); + }); } // Cheats route @@ -1876,33 +1883,39 @@ function doChangeSupportedSyndicate() { } function doAddCurrency(currency) { - $.post({ - url: "/custom/addCurrency?" + window.authz, - contentType: "application/json", - data: JSON.stringify({ - currency, - delta: document.getElementById(currency + "-delta").valueAsNumber - }) - }).then(function () { - updateInventory(); + revalidateAuthz(() => { + $.post({ + url: "/custom/addCurrency?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + currency, + delta: document.getElementById(currency + "-delta").valueAsNumber + }) + }).then(function () { + updateInventory(); + }); }); } function doQuestUpdate(operation, itemType) { - $.post({ - url: "/custom/manageQuests?" + window.authz + "&operation=" + operation + "&itemType=" + itemType, - contentType: "application/json" - }).then(function () { - updateInventory(); + revalidateAuthz(() => { + $.post({ + url: "/custom/manageQuests?" + window.authz + "&operation=" + operation + "&itemType=" + itemType, + contentType: "application/json" + }).then(function () { + updateInventory(); + }); }); } function doBulkQuestUpdate(operation) { - $.post({ - url: "/custom/manageQuests?" + window.authz + "&operation=" + operation, - contentType: "application/json" - }).then(function () { - updateInventory(); + revalidateAuthz(() => { + $.post({ + url: "/custom/manageQuests?" + window.authz + "&operation=" + operation, + contentType: "application/json" + }).then(function () { + updateInventory(); + }); }); } From 0af7f4120163a773c1ad5435ca7e1799a1396f45 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:27:47 -0700 Subject: [PATCH 657/776] fix: unset LibraryPersonalTarget after completing it (#1913) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1913 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 35486d21..66328a0d 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -380,6 +380,7 @@ export const addMissionInventoryUpdates = async ( : 10) ) { progress.Completed = true; + inventory.LibraryPersonalTarget = undefined; } logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`); synthesisIgnored = false; From 9468768947aad5f500631aa786e3096071d403a2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:28:01 -0700 Subject: [PATCH 658/776] fix: weapon seed's low dword being sign extended (#1914) JavaScript's semantics here are incredibly stupid, but basically if the initial DWORD's high WORD's MSB is true, the number would become negative after the shift left by 16. Then when ORing it with the highDword, the initial DWORD would be sign-extended to a QWORD, meaning the high DWORD would become all 1s, basically cancelling out the entire OR operation. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1914 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 82ceae46..171c60fc 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -286,7 +286,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); if (vendorInfo.RandomSeedType == "VRST_WEAPON") { const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); - item.LocTagRandSeed = (BigInt(highDword) << 32n) | BigInt(item.LocTagRandSeed); + item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); } } processed.ItemManifest.push(item); From c06abded11bb3bad5c4f1f390f289f9bb0f3c3e5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:27:36 -0700 Subject: [PATCH 659/776] fix: always multiply acquired gear quantity by purchaseQuantity (#1924) Closes #1915 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1924 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 | 4 ++-- src/services/inventoryService.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 7adc2d3d..7462de3e 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -11,7 +11,7 @@ import { import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; -import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; +import { ExportFlavour } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; @@ -50,7 +50,7 @@ export const inboxController: RequestHandler = async (req, res) => { inventory, attachmentItems.map(attItem => ({ ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem, - ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1 + ItemCount: 1 })), inventoryChanges ); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index da8b0848..6c5a4b06 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -486,6 +486,10 @@ export const addItem = async ( }; } if (typeName in ExportGear) { + // Multipling by purchase quantity for gear because: + // - The Saya's Vigil scanner message has it as a non-counted attachment. + // - Blueprints for Ancient Protector Specter, Shield Osprey Specter, etc. have num=1 despite giving their purchaseQuantity. + quantity *= ExportGear[typeName].purchaseQuantity ?? 1; const consumablesChanges = [ { ItemType: typeName, From 3de68e51d5fccabb4a64d7209b33c11c9fc54886 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:27:47 -0700 Subject: [PATCH 660/776] fix: properly set Harvestable & DeathSquadable fields (#1925) Closes #1916 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1925 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 4 +- src/services/missionInventoryUpdateService.ts | 54 ++++++++----------- src/types/requestTypes.ts | 1 + 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index dcb5a536..f3c8f0d8 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1688,9 +1688,9 @@ const inventorySchema = new Schema( //Like BossAladV,BossCaptainVor come for you on missions % chance DeathMarks: { type: [String], default: [] }, //Zanuka - Harvestable: Boolean, + Harvestable: { type: Boolean, default: true }, //Grustag three - DeathSquadable: Boolean, + DeathSquadable: { type: Boolean, default: true }, EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 66328a0d..18073ce8 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -143,38 +143,6 @@ export const addMissionInventoryUpdates = async ( ]); } } - - // Somewhat heuristically detect G3 capture: - // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 - // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694 - // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1724 - if ( - inventoryUpdates.MissionFailed && - inventoryUpdates.MissionStatus == "GS_FAILURE" && - inventoryUpdates.ObjectiveReached && - !inventoryUpdates.LockedWeaponGroup && - !inventory.LockedWeaponGroup && - !inventoryUpdates.LevelKeyName - ) { - const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; - const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; - const SuitId = new Types.ObjectId(config.s!.ItemId.$oid); - - inventory.BrandedSuits ??= []; - if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) { - inventory.BrandedSuits.push(SuitId); - - await createMessage(inventory.accountOwnerId, [ - { - sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", - msg: "/Lotus/Language/G1Quests/BrandedMessage", - sub: "/Lotus/Language/G1Quests/BrandedTitle", - att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], - highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. - } - ]); - } - } } if (inventoryUpdates.RewardInfo) { if (inventoryUpdates.RewardInfo.periodicMissionTag) { @@ -537,6 +505,23 @@ export const addMissionInventoryUpdates = async ( } break; } + case "BrandedSuits": { + inventory.BrandedSuits ??= []; + if (!inventory.BrandedSuits.find(x => x.equals(value.$oid))) { + inventory.BrandedSuits.push(new Types.ObjectId(value.$oid)); + + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + msg: "/Lotus/Language/G1Quests/BrandedMessage", + sub: "/Lotus/Language/G1Quests/BrandedTitle", + att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], + highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. + } + ]); + } + break; + } case "LockedWeaponGroup": { inventory.LockedWeaponGroup = { s: new Types.ObjectId(value.s.$oid), @@ -545,12 +530,17 @@ export const addMissionInventoryUpdates = async ( m: value.m ? new Types.ObjectId(value.m.$oid) : undefined, sn: value.sn ? new Types.ObjectId(value.sn.$oid) : undefined }; + inventory.Harvestable = false; break; } case "UnlockWeapons": { inventory.LockedWeaponGroup = undefined; break; } + case "IncHarvester": { + inventory.Harvestable = true; + break; + } case "CurrentLoadOutIds": { if (value.LoadOuts) { const loadout = await Loadout.findOne({ loadoutOwnerId: inventory.accountOwnerId }); diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 976c48f5..e47b5b86 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -130,6 +130,7 @@ export type IMissionInventoryUpdateRequest = { }[]; KubrowPetEggs?: IKubrowPetEggClient[]; DiscoveredMarkers?: IDiscoveredMarker[]; + BrandedSuits?: IOid; // sent when captured by g3 LockedWeaponGroup?: ILockedWeaponGroupClient; // sent when captured by zanuka UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture IncHarvester?: boolean; // sent when recovered weapons from zanuka capture From 660768b53b254bc8deeef0403b3559f9df9956f3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:28:01 -0700 Subject: [PATCH 661/776] fix: handle DuviriInfo being absent from inventory (#1926) Closes #1917 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1926 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 | 9 ++++++++- src/models/inventoryModels/inventoryModel.ts | 4 ++-- src/services/missionInventoryUpdateService.ts | 2 +- src/types/inventoryTypes/inventoryTypes.ts | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index e9b15a63..4f73a5f7 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -106,7 +106,14 @@ export const inventoryController: RequestHandler = async (request, response) => const currentDuviriMood = Math.trunc(Date.now() / 7200000); if (lastSyncDuviriMood != currentDuviriMood) { logger.debug(`refreshing duviri seed`); - inventory.DuviriInfo.Seed = generateRewardSeed(); + if (!inventory.DuviriInfo) { + inventory.DuviriInfo = { + Seed: generateRewardSeed(), + NumCompletions: 0 + }; + } else { + inventory.DuviriInfo.Seed = generateRewardSeed(); + } } } inventory.LastInventorySync = new Types.ObjectId(); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index f3c8f0d8..8c53d608 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -391,8 +391,8 @@ MailboxSchema.set("toJSON", { const DuviriInfoSchema = new Schema( { - Seed: BigInt, - NumCompletions: { type: Number, default: 0 } + Seed: { type: BigInt, required: true }, + NumCompletions: { type: Number, required: true } }, { _id: false, diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 18073ce8..0b0b3f72 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -601,7 +601,7 @@ export const addMissionInventoryUpdates = async ( case "duviriCaveOffers": { // Duviri cave offers (generated with the duviri seed) change after completing one of its game modes (not when aborting). if (inventoryUpdates.MissionStatus != "GS_QUIT") { - inventory.DuviriInfo.Seed = generateRewardSeed(); + inventory.DuviriInfo!.Seed = generateRewardSeed(); } break; } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 4973ff87..92b6973e 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -202,7 +202,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu OperatorLoadOuts: IOperatorConfigClient[]; KahlLoadOuts: IOperatorConfigClient[]; - DuviriInfo: IDuviriInfo; + DuviriInfo?: IDuviriInfo; Mailbox?: IMailboxClient; SubscribedToEmails: number; Created: IMongoDate; From 3d6c880c96537a4b34cd3622757815dc2eef5ade Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:28:16 -0700 Subject: [PATCH 662/776] feat: handle client setting InfestationDate on equipment (#1927) Closes #1919 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1927 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/addXpController.ts | 4 ++-- src/helpers/inventoryHelpers.ts | 4 ++++ src/services/inventoryService.ts | 18 +++++++++++------- src/services/missionInventoryUpdateService.ts | 5 ++--- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/controllers/custom/addXpController.ts b/src/controllers/custom/addXpController.ts index 7cb284fe..0ca05102 100644 --- a/src/controllers/custom/addXpController.ts +++ b/src/controllers/custom/addXpController.ts @@ -1,4 +1,4 @@ -import { addGearExpByCategory, getInventory } from "@/src/services/inventoryService"; +import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -20,7 +20,7 @@ export const addXpController: RequestHandler = async (req, res) => { } } } - addGearExpByCategory(inventory, gear, category as TEquipmentKey); + applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey); } await inventory.save(); res.end(); diff --git a/src/helpers/inventoryHelpers.ts b/src/helpers/inventoryHelpers.ts index 79466fb3..4dd38c5a 100644 --- a/src/helpers/inventoryHelpers.ts +++ b/src/helpers/inventoryHelpers.ts @@ -10,6 +10,10 @@ export const toMongoDate = (date: Date): IMongoDate => { return { $date: { $numberLong: date.getTime().toString() } }; }; +export const fromMongoData = (date: IMongoDate): Date => { + return new Date(parseInt(date.$date.$numberLong)); +}; + export const kubrowWeights: Record = { COMMON: 6, UNCOMMON: 4, diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6c5a4b06..043383b1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -69,6 +69,7 @@ import { import { createShip } from "./shipService"; import { catbrowDetails, + fromMongoData, kubrowDetails, kubrowFurPatternsWeights, kubrowWeights, @@ -1475,21 +1476,20 @@ export const addEmailItem = async ( return inventoryChanges; }; -//TODO: wrong id is not erroring -export const addGearExpByCategory = ( +export const applyClientEquipmentUpdates = ( inventory: TInventoryDatabaseDocument, gearArray: IEquipmentClient[], categoryName: TEquipmentKey ): void => { const category = inventory[categoryName]; - gearArray.forEach(({ ItemId, XP }) => { - if (!XP) { - return; + gearArray.forEach(({ ItemId, XP, InfestationDate }) => { + const item = category.id(ItemId.$oid); + if (!item) { + throw new Error(`No item with id ${ItemId.$oid} in ${categoryName}`); } - const item = category.id(ItemId.$oid); - if (item) { + if (XP) { item.XP ??= 0; item.XP += XP; @@ -1504,6 +1504,10 @@ export const addGearExpByCategory = ( }); } } + + if (InfestationDate) { + item.InfestationDate = fromMongoData(InfestationDate); + } }); }; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 0b0b3f72..dc670c59 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -21,7 +21,6 @@ import { addFocusXpIncreases, addFusionPoints, addFusionTreasures, - addGearExpByCategory, addItem, addLevelKeys, addLoreFragmentScans, @@ -32,6 +31,7 @@ import { addShipDecorations, addSkin, addStanding, + applyClientEquipmentUpdates, combineInventoryChanges, generateRewardSeed, getCalendarProgress, @@ -660,9 +660,8 @@ export const addMissionInventoryUpdates = async ( } break; default: - // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { - addGearExpByCategory(inventory, value as IEquipmentClient[], key as TEquipmentKey); + applyClientEquipmentUpdates(inventory, value as IEquipmentClient[], key as TEquipmentKey); } break; // if ( From ed54e00a03127929e6df11b7ebd08a304080b638 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:28:24 -0700 Subject: [PATCH 663/776] fix: compatibility with echoes of duviri (#1928) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1928 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 | 15 ++++++++++++--- src/services/worldStateService.ts | 22 ++++++++++++++++++++++ src/types/worldStateTypes.ts | 13 +++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 7462de3e..94d253ad 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -14,6 +14,7 @@ import { logger } from "@/src/utils/logger"; import { ExportFlavour } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; +import { IOid } from "@/src/types/commonTypes"; export const inboxController: RequestHandler = async (req, res) => { const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; @@ -28,10 +29,10 @@ export const inboxController: RequestHandler = async (req, res) => { return; } - await deleteMessageRead(deleteId as string); + await deleteMessageRead(parseOid(deleteId as string)); res.status(200).end(); } else if (messageId) { - const message = await getMessage(messageId as string); + const message = await getMessage(parseOid(messageId as string)); message.r = true; await message.save(); @@ -100,7 +101,7 @@ export const inboxController: RequestHandler = async (req, res) => { await createNewEventMessages(req); const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 }); - const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId); + const latestClientMessage = messages.find(m => m._id.toString() === parseOid(latestClientMessageId as string)); if (!latestClientMessage) { logger.debug(`this should only happen after DeleteAllRead `); @@ -123,3 +124,11 @@ export const inboxController: RequestHandler = async (req, res) => { res.json({ Inbox: inbox }); } }; + +// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb +const parseOid = (oid: string): string => { + if (oid[0] == "{") { + return (JSON.parse(oid) as IOid).$oid; + } + return oid; +}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index e2e2ea23..9350360e 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -724,6 +724,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => { SyndicateMissions: [...staticWorldState.SyndicateMissions] }; + // Omit void fissures for versions prior to Whispers in the Walls to avoid errors with the unknown deimos nodes having void fissures. + if (buildLabel && version_compare(buildLabel, "2023.11.06.13.39") <= 0) { + worldState.ActiveMissions = []; + } + if (config.worldState?.starDays) { worldState.Goals.push({ _id: { $oid: "67a4dcce2a198564d62e1647" }, @@ -1227,3 +1232,20 @@ export const isArchwingMission = (node: IRegion): boolean => { } return false; }; + +export const version_compare = (a: string, b: string): number => { + const a_digits = a + .split("/")[0] + .split(".") + .map(x => parseInt(x)); + const b_digits = b + .split("/")[0] + .split(".") + .map(x => parseInt(x)); + for (let i = 0; i != a_digits.length; ++i) { + if (a_digits[i] != b_digits[i]) { + return a_digits[i] > b_digits[i] ? 1 : -1; + } + } + return 0; +}; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 303c3c33..1e8d4033 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -10,6 +10,7 @@ export interface IWorldState { LiteSorties: ILiteSortie[]; SyndicateMissions: ISyndicateMissionInfo[]; GlobalUpgrades: IGlobalUpgrade[]; + ActiveMissions: IFissure[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; SeasonInfo: { @@ -71,6 +72,18 @@ export interface IGlobalUpgrade { LocalizeDescTag: string; } +export interface IFissure { + _id: IOid; + Region: number; + Seed: number; + Activation: IMongoDate; + Expiry: IMongoDate; + Node: string; + MissionType: string; + Modifier: string; + Hard?: boolean; +} + export interface INodeOverride { _id: IOid; Activation?: IMongoDate; From 7d02906656324900f99d3aa9b2599cd724fd44fe Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:28:34 -0700 Subject: [PATCH 664/776] fix: better handling of assassination missions in sorties (#1930) Closes #1918 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1930 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/rngService.ts | 6 ++-- src/services/worldStateService.ts | 59 ++++++++++++++----------------- 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 68225ca1..0e836466 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -6,7 +6,7 @@ export interface IRngResult { probability: number; } -export const getRandomElement = (arr: T[]): T | undefined => { +export const getRandomElement = (arr: readonly T[]): T | undefined => { return arr[Math.floor(Math.random() * arr.length)]; }; @@ -113,7 +113,7 @@ export class CRng { return min; } - randomElement(arr: T[]): T | undefined { + randomElement(arr: readonly T[]): T | undefined { return arr[Math.floor(this.random() * arr.length)]; } @@ -145,7 +145,7 @@ export class SRng { return min; } - randomElement(arr: T[]): T | undefined { + randomElement(arr: readonly T[]): T | undefined { return arr[this.randomInt(0, arr.length - 1)]; } diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 9350360e..a8b4cf5a 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -33,9 +33,11 @@ const sortieBosses = [ "SORTIE_BOSS_LEPHANTIS", "SORTIE_BOSS_INFALAD", "SORTIE_BOSS_CORRUPTED_VOR" -]; +] as const; -const sortieBossToFaction: Record = { +type TSortieBoss = (typeof sortieBosses)[number]; + +const sortieBossToFaction: Record = { SORTIE_BOSS_HYENA: "FC_CORPUS", SORTIE_BOSS_KELA: "FC_GRINEER", SORTIE_BOSS_VOR: "FC_GRINEER", @@ -74,21 +76,22 @@ const sortieFactionToSpecialMissionTileset: Record = { FC_INFESTATION: "CorpusShipTileset" }; -const sortieBossNode: Record = { - SORTIE_BOSS_HYENA: "SolNode127", - SORTIE_BOSS_KELA: "SolNode193", - SORTIE_BOSS_VOR: "SolNode108", - SORTIE_BOSS_RUK: "SolNode32", - SORTIE_BOSS_HEK: "SolNode24", - SORTIE_BOSS_KRIL: "SolNode99", - SORTIE_BOSS_TYL: "SolNode105", - SORTIE_BOSS_JACKAL: "SolNode104", +const sortieBossNode: Record, string> = { SORTIE_BOSS_ALAD: "SolNode53", SORTIE_BOSS_AMBULAS: "SolNode51", - SORTIE_BOSS_NEF: "SettlementNode20", - SORTIE_BOSS_RAPTOR: "SolNode210", + SORTIE_BOSS_HEK: "SolNode24", + SORTIE_BOSS_HYENA: "SolNode127", + SORTIE_BOSS_INFALAD: "SolNode166", + SORTIE_BOSS_JACKAL: "SolNode104", + SORTIE_BOSS_KELA: "SolNode193", + SORTIE_BOSS_KRIL: "SolNode99", SORTIE_BOSS_LEPHANTIS: "SolNode712", - SORTIE_BOSS_INFALAD: "SolNode705" + SORTIE_BOSS_NEF: "SettlementNode20", + SORTIE_BOSS_PHORID: "SolNode171", + SORTIE_BOSS_RAPTOR: "SolNode210", + SORTIE_BOSS_RUK: "SolNode32", + SORTIE_BOSS_TYL: "SolNode105", + SORTIE_BOSS_VOR: "SolNode108" }; const eidolonJobs = [ @@ -270,6 +273,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { key in sortieTilesets ) { if ( + value.missionIndex != 0 && // Assassination will be decided independently value.missionIndex != 5 && // Sorties do not have capture missions !availableMissionIndexes.includes(value.missionIndex) ) { @@ -310,28 +314,17 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { sortieFactionToSpecialMissionTileset[sortieBossToFaction[boss]] ); - if (i == 2 && rng.randomInt(0, 2) == 2) { + if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) { const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); const modifierType = rng.randomElement(filteredModifiers)!; - if (boss == "SORTIE_BOSS_PHORID") { - selectedNodes.push({ - missionType: "MT_ASSASSINATION", - modifierType, - node, - tileset: sortieTilesets[node as keyof typeof sortieTilesets] - }); - nodes.splice(randomIndex, 1); - continue; - } else if (sortieBossNode[boss]) { - selectedNodes.push({ - missionType: "MT_ASSASSINATION", - modifierType, - node: sortieBossNode[boss], - tileset: sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] - }); - continue; - } + selectedNodes.push({ + missionType: "MT_ASSASSINATION", + modifierType, + node: sortieBossNode[boss], + tileset: sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] + }); + continue; } const missionType = eMissionType[missionIndex].tag; From 2c3043f40ea323c1cfd6d66f21baaafe7a306c92 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:45:42 -0700 Subject: [PATCH 665/776] fix: login failure on U32 veilbreaker (#1932) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1932 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 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 565bebf0..09fca8c7 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -7,6 +7,7 @@ import { Account } from "@/src/models/loginModel"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; import { logger } from "@/src/utils/logger"; +import { version_compare } from "@/src/services/worldStateService"; 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 @@ -94,7 +95,7 @@ export const loginController: RequestHandler = async (request, response) => { }; const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { - return { + const resp: ILoginResponse = { id: account.id, DisplayName: account.DisplayName, CountryCode: account.CountryCode, @@ -108,11 +109,14 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b Nonce: account.Nonce, Groups: [], IRC: config.myIrcAddresses ?? [myAddress], - platformCDNs: [`https://${myAddress}/`], HUB: `https://${myAddress}/api/`, NRS: config.NRS, DTLS: 99, BuildLabel: buildLabel, MatchmakingBuildId: buildConfig.matchmakingBuildId }; + if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { + resp.platformCDNs = [`https://${myAddress}/`]; + } + return resp; }; From c4b2248df5e77c52c2b6a027fc492020df90fe18 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 30 Apr 2025 23:50:02 -0700 Subject: [PATCH 666/776] fix: login failure on U31.5 angels of the zariman (#1933) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1933 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 | 14 ++++++++------ src/types/loginTypes.ts | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 09fca8c7..cf5f759f 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -100,7 +100,6 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b DisplayName: account.DisplayName, CountryCode: account.CountryCode, ClientType: account.ClientType, - CrossPlatformAllowed: account.CrossPlatformAllowed, ForceLogoutVersion: account.ForceLogoutVersion, AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, @@ -109,14 +108,17 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b Nonce: account.Nonce, Groups: [], IRC: config.myIrcAddresses ?? [myAddress], - HUB: `https://${myAddress}/api/`, NRS: config.NRS, DTLS: 99, - BuildLabel: buildLabel, - MatchmakingBuildId: buildConfig.matchmakingBuildId + BuildLabel: buildLabel }; - if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { - resp.platformCDNs = [`https://${myAddress}/`]; + if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { + resp.CrossPlatformAllowed = account.CrossPlatformAllowed; + resp.HUB = `https://${myAddress}/api/`; + resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; + if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { + resp.platformCDNs = [`https://${myAddress}/`]; + } } return resp; }; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 17128e2a..654bc4dc 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -4,7 +4,7 @@ export interface IAccountAndLoginResponseCommons { DisplayName: string; CountryCode: string; ClientType: string; - CrossPlatformAllowed: boolean; + CrossPlatformAllowed?: boolean; ForceLogoutVersion: number; AmazonAuthToken?: string; AmazonRefreshToken?: string; @@ -46,7 +46,7 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons { id: string; Groups: IGroup[]; BuildLabel: string; - MatchmakingBuildId: string; + MatchmakingBuildId?: string; platformCDNs?: string[]; NRS?: string[]; DTLS: number; From 8eefd67d79d8f2d5e3adf8efa184205417cccca0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:09:45 +0200 Subject: [PATCH 667/776] chore: fix typo --- src/helpers/inventoryHelpers.ts | 2 +- src/services/inventoryService.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/inventoryHelpers.ts b/src/helpers/inventoryHelpers.ts index 4dd38c5a..5efc801d 100644 --- a/src/helpers/inventoryHelpers.ts +++ b/src/helpers/inventoryHelpers.ts @@ -10,7 +10,7 @@ export const toMongoDate = (date: Date): IMongoDate => { return { $date: { $numberLong: date.getTime().toString() } }; }; -export const fromMongoData = (date: IMongoDate): Date => { +export const fromMongoDate = (date: IMongoDate): Date => { return new Date(parseInt(date.$date.$numberLong)); }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 043383b1..089e2fbb 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -69,7 +69,7 @@ import { import { createShip } from "./shipService"; import { catbrowDetails, - fromMongoData, + fromMongoDate, kubrowDetails, kubrowFurPatternsWeights, kubrowWeights, @@ -1506,7 +1506,7 @@ export const applyClientEquipmentUpdates = ( } if (InfestationDate) { - item.InfestationDate = fromMongoData(InfestationDate); + item.InfestationDate = fromMongoDate(InfestationDate); } }); }; From 12b6e5d16eae98e52139a126d8beca9f40d1af06 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 05:23:25 -0700 Subject: [PATCH 668/776] fix: login failure on U31 the new war & U30.5 sisters of parvos (#1943) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1943 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 | 16 +++++++++------- src/types/loginTypes.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index cf5f759f..0d595e95 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -99,7 +99,6 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b id: account.id, DisplayName: account.DisplayName, CountryCode: account.CountryCode, - ClientType: account.ClientType, ForceLogoutVersion: account.ForceLogoutVersion, AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, @@ -112,12 +111,15 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b DTLS: 99, BuildLabel: buildLabel }; - if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { - resp.CrossPlatformAllowed = account.CrossPlatformAllowed; - resp.HUB = `https://${myAddress}/api/`; - resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; - if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { - resp.platformCDNs = [`https://${myAddress}/`]; + if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { + resp.ClientType = account.ClientType; + if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { + resp.CrossPlatformAllowed = account.CrossPlatformAllowed; + resp.HUB = `https://${myAddress}/api/`; + resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; + if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { + resp.platformCDNs = [`https://${myAddress}/`]; + } } } return resp; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 654bc4dc..79cf538d 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -3,7 +3,7 @@ import { Types } from "mongoose"; export interface IAccountAndLoginResponseCommons { DisplayName: string; CountryCode: string; - ClientType: string; + ClientType?: string; CrossPlatformAllowed?: boolean; ForceLogoutVersion: number; AmazonAuthToken?: string; From cddd4bdf5c68ca3b5da11fea71b76cea0b7e76fe Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:51:31 -0700 Subject: [PATCH 669/776] fix: filter sortie armor/shields modifier based on mission faction (#1934) Closes #1931 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1934 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 86 ++++++++++++++++--------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index a8b4cf5a..634fc10e 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -230,40 +230,6 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { const boss = rng.randomElement(sortieBosses)!; - const modifiers = [ - "SORTIE_MODIFIER_LOW_ENERGY", - "SORTIE_MODIFIER_IMPACT", - "SORTIE_MODIFIER_SLASH", - "SORTIE_MODIFIER_PUNCTURE", - "SORTIE_MODIFIER_EXIMUS", - "SORTIE_MODIFIER_MAGNETIC", - "SORTIE_MODIFIER_CORROSIVE", - "SORTIE_MODIFIER_VIRAL", - "SORTIE_MODIFIER_ELECTRICITY", - "SORTIE_MODIFIER_RADIATION", - "SORTIE_MODIFIER_GAS", - "SORTIE_MODIFIER_FIRE", - "SORTIE_MODIFIER_EXPLOSION", - "SORTIE_MODIFIER_FREEZE", - "SORTIE_MODIFIER_TOXIN", - "SORTIE_MODIFIER_POISON", - "SORTIE_MODIFIER_HAZARD_RADIATION", - "SORTIE_MODIFIER_HAZARD_MAGNETIC", - "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest - "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon - "SORTIE_MODIFIER_HAZARD_ICE", - "SORTIE_MODIFIER_HAZARD_COLD", - "SORTIE_MODIFIER_SECONDARY_ONLY", - "SORTIE_MODIFIER_SHOTGUN_ONLY", - "SORTIE_MODIFIER_SNIPER_ONLY", - "SORTIE_MODIFIER_RIFLE_ONLY", - "SORTIE_MODIFIER_MELEE_ONLY", - "SORTIE_MODIFIER_BOW_ONLY" - ]; - - if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS"); - if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR"); - const nodes: string[] = []; const availableMissionIndexes: number[] = []; for (const [key, value] of Object.entries(ExportRegions)) { @@ -314,9 +280,38 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { sortieFactionToSpecialMissionTileset[sortieBossToFaction[boss]] ); + const modifiers = [ + "SORTIE_MODIFIER_LOW_ENERGY", + "SORTIE_MODIFIER_IMPACT", + "SORTIE_MODIFIER_SLASH", + "SORTIE_MODIFIER_PUNCTURE", + "SORTIE_MODIFIER_EXIMUS", + "SORTIE_MODIFIER_MAGNETIC", + "SORTIE_MODIFIER_CORROSIVE", + "SORTIE_MODIFIER_VIRAL", + "SORTIE_MODIFIER_ELECTRICITY", + "SORTIE_MODIFIER_RADIATION", + "SORTIE_MODIFIER_GAS", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_TOXIN", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_HAZARD_RADIATION", + "SORTIE_MODIFIER_HAZARD_MAGNETIC", + "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest + "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon + "SORTIE_MODIFIER_HAZARD_ICE", + "SORTIE_MODIFIER_HAZARD_COLD", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) { - const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY"); - const modifierType = rng.randomElement(filteredModifiers)!; + const modifierType = rng.randomElement(modifiers)!; selectedNodes.push({ missionType: "MT_ASSASSINATION", @@ -334,12 +329,21 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { continue; } - const filteredModifiers = - missionType === "MT_TERRITORY" - ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION") - : modifiers; + modifiers.push("SORTIE_MODIFIER_MELEE_ONLY"); // not an assassination mission, can now push this - const modifierType = rng.randomElement(filteredModifiers)!; + if (missionType != "MT_TERRITORY") { + modifiers.push("SORTIE_MODIFIER_HAZARD_RADIATION"); + } + + if (ExportRegions[node].factionIndex == 0) { + // Grineer + modifiers.push("SORTIE_MODIFIER_ARMOR"); + } else if (ExportRegions[node].factionIndex == 1) { + // Corpus + modifiers.push("SORTIE_MODIFIER_SHIELDS"); + } + + const modifierType = rng.randomElement(modifiers)!; selectedNodes.push({ missionType, From 19b04533df4f83ccc852c43e0818932cfc19aa6d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:52:43 -0700 Subject: [PATCH 670/776] fix: omit void fissures for U35.1 (#1935) This version also has a script error even tho it should know most of the new deimos nodes... Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1935 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, 2 insertions(+), 2 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 634fc10e..77650d0f 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -721,8 +721,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { SyndicateMissions: [...staticWorldState.SyndicateMissions] }; - // Omit void fissures for versions prior to Whispers in the Walls to avoid errors with the unknown deimos nodes having void fissures. - if (buildLabel && version_compare(buildLabel, "2023.11.06.13.39") <= 0) { + // Omit void fissures for versions prior to Dante Unbound to avoid script errors. + if (buildLabel && version_compare(buildLabel, "2024.03.24.20.00") < 0) { worldState.ActiveMissions = []; } From 9b652f5c3c7a9eba2d641cd334dae31e0c48a1fc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:53:10 -0700 Subject: [PATCH 671/776] fix: spoof nemesis to avoid script errors in older versions (#1936) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1936 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 | 23 ++++++++++--- src/controllers/api/loginController.ts | 4 ++- .../api/missionInventoryUpdateController.ts | 10 +++--- src/helpers/nemesisHelpers.ts | 32 ++++++++++++++++--- src/models/loginModel.ts | 1 + src/types/loginTypes.ts | 1 + 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 4f73a5f7..8febdcf7 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; import allDialogue from "@/static/fixed_responses/allDialogue.json"; @@ -24,11 +24,12 @@ import { import { logger } from "@/src/utils/logger"; import { catBreadHash } from "@/src/helpers/stringHelpers"; import { Types } from "mongoose"; +import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers"; export const inventoryController: RequestHandler = async (request, response) => { - const accountId = await getAccountIdForRequest(request); + const account = await getAccountForRequest(request); - const inventory = await Inventory.findOne({ accountOwnerId: accountId }); + const inventory = await Inventory.findOne({ accountOwnerId: account._id }); if (!inventory) { response.status(400).json({ error: "inventory was undefined" }); @@ -119,12 +120,15 @@ export const inventoryController: RequestHandler = async (request, response) => inventory.LastInventorySync = new Types.ObjectId(); await inventory.save(); - response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query)); + response.json( + await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query, account.BuildLabel) + ); }; export const getInventoryResponse = async ( inventory: TInventoryDatabaseDocument, - xpBasedLevelCapDisabled: boolean + xpBasedLevelCapDisabled: boolean, + buildLabel: string | undefined ): Promise => { const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>( "LoadOutPresets" @@ -299,6 +303,15 @@ export const getInventoryResponse = async ( // Set 2FA enabled so trading post can be used inventoryResponse.HWIDProtectEnabled = true; + // Fix nemesis for older versions + if ( + inventoryResponse.Nemesis && + buildLabel && + !isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel) + ) { + inventoryResponse.Nemesis = undefined; + } + return inventoryResponse; }; diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 0d595e95..366e87c3 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -47,7 +47,8 @@ export const loginController: RequestHandler = async (request, response) => { ForceLogoutVersion: 0, ConsentNeeded: false, TrackedSettings: [], - Nonce: nonce + Nonce: nonce, + BuildLabel: buildLabel }); logger.debug("created new account"); response.json(createLoginResponse(myAddress, newAccount, buildLabel)); @@ -88,6 +89,7 @@ export const loginController: RequestHandler = async (request, response) => { account.ClientType = loginRequest.ClientType; account.Nonce = nonce; account.CountryCode = loginRequest.lang.toUpperCase(); + account.BuildLabel = buildLabel; } await account.save(); diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index ff181e0b..5e642c27 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -1,6 +1,6 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService"; import { generateRewardSeed, getInventory } from "@/src/services/inventoryService"; @@ -49,11 +49,11 @@ import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes"; */ //move credit calc in here, return MissionRewards: [] if no reward info export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise => { - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); const missionReport = getJSONfromString((req.body as string).toString()); logger.debug("mission report:", missionReport); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const firstCompletion = missionReport.SortieId ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 : false; @@ -65,7 +65,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) ) { inventory.RewardSeed = generateRewardSeed(); await inventory.save(); - const inventoryResponse = await getInventoryResponse(inventory, true); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); res.json({ InventoryJson: JSON.stringify(inventoryResponse), MissionRewards: [] @@ -84,7 +84,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) inventory.RewardSeed = generateRewardSeed(); await inventory.save(); - const inventoryResponse = await getInventoryResponse(inventory, true); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); //TODO: figure out when to send inventory. it is needed for many cases. res.json({ diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 3a146831..5df1ad9a 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -6,7 +6,7 @@ import { logger } from "../utils/logger"; import { IOid } from "../types/commonTypes"; import { Types } from "mongoose"; import { addMods, generateRewardSeed } from "../services/inventoryService"; -import { isArchwingMission } from "../services/worldStateService"; +import { isArchwingMission, version_compare } from "../services/worldStateService"; import { fromStoreItem, toStoreItem } from "../services/itemDataService"; import { createMessage } from "../services/inboxService"; @@ -237,15 +237,39 @@ const corpusVersionThreeWeapons = [ export const getWeaponsForManifest = (manifest: string): readonly string[] => { switch (manifest) { - case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": + case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": // >= 35.6.0 return kuvaLichVersionSixWeapons; - case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": - case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": + case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": // >= 35.6.0 + case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": // >= 37.0.0 return corpusVersionThreeWeapons; } throw new Error(`unknown nemesis manifest: ${manifest}`); }; +export const isNemesisCompatibleWithVersion = ( + nemesis: { manifest: string; Faction: string }, + buildLabel: string +): boolean => { + // Anything below 35.6.0 is not going to be okay given our set of supported manifests. + if (version_compare(buildLabel, "2024.05.15.11.07") < 0) { + return false; + } + + if (nemesis.Faction == "FC_INFESTATION") { + // Anything below 38.5.0 isn't gonna like an infested lich. + if (version_compare(buildLabel, "2025.03.18.16.07") < 0) { + return false; + } + } else if (nemesis.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour") { + // Anything below 37.0.0 isn't gonna know version 4, but version 3 is identical in terms of weapon choices, so we can spoof it to that. + if (version_compare(buildLabel, "2024.10.01.11.03") < 0) { + nemesis.manifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"; + } + } + + return true; +}; + export const getInnateDamageTag = ( KillingSuit: string ): diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 3e564aa6..fe9d0b5f 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -20,6 +20,7 @@ const databaseAccountSchema = new Schema( ConsentNeeded: { type: Boolean, required: true }, TrackedSettings: { type: [String], default: [] }, Nonce: { type: Number, default: 0 }, + BuildLabel: String, Dropped: Boolean, LatestEventMessageDate: { type: Date, default: 0 }, LastLoginRewardDate: { type: Number, default: 0 }, diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 79cf538d..dfdebe0e 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -16,6 +16,7 @@ export interface IAccountAndLoginResponseCommons { export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons { email: string; password: string; + BuildLabel?: string; } export interface IDatabaseAccount extends IDatabaseAccountRequiredFields { From 49834172014b1269ec759d6e3615526f53941d94 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:53:28 -0700 Subject: [PATCH 672/776] chore: update certificate (#1937) This is good for *.faketls.com until March 2026. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1937 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/certs/cert.pem | 58 +++++++++++++++++++++---------------------- static/certs/key.pem | 52 +++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/static/certs/cert.pem b/static/certs/cert.pem index 4f043e8a..4bce415b 100644 --- a/static/certs/cert.pem +++ b/static/certs/cert.pem @@ -1,38 +1,38 @@ -----BEGIN CERTIFICATE----- -MIIGLzCCBRegAwIBAgIRAILIyLcitteoEGcJt1QBXvcwDQYJKoZIhvcNAQELBQAw -gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO -BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE -AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD -QTAeFw0yNDA4MDIwMDAwMDBaFw0yNTA4MDIyMzU5NTlaMBcxFTATBgNVBAMMDCou -dmlhdGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMTToSjY -3aUIxjghIkikJfFExVwSEzIM2XaQNJE+SxQ6Cc+xUR5QJrMJnM/39sH5c5imMEUo -2OnstCIaVMPx5ZPN+HXLsvmoVAe2/xYe7emnZ5ZFTUXPyqkzDRg0hkMJiWWo/Nmf -ypZfUJoz6hVkXwsgNFPTVuo7aECQFlZslh2HQVDOfBaNBxQBaOJ5vf6nllf/aLyB -tZ74nlLynVYV9kYzISP4dUcxQ+D4HZgIxyOQfcN3EHUS1ZVaIp8hupOygF8zGQyJ -uzFozzg5I59U+hT1yQG3FlwTBnP+sA0+hW0LBTbWSISm0If1SgHlUEqxLlosjuTG -BG45h9o2bAz9po0CAwEAAaOCAvswggL3MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb -+ZsF4bgBjWHhMB0GA1UdDgQWBBQ/OeA2gLbVIKIuIitYRqSRUWMP3TAOBgNVHQ8B +MIIGMDCCBRigAwIBAgIQX4800cgswlDH/QexMSnnnjANBgkqhkiG9w0BAQsFADCB +jzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD +Ey5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENB +MB4XDTI1MDMwNjAwMDAwMFoXDTI2MDMwNjIzNTk1OVowGDEWMBQGA1UEAwwNKi5m +YWtldGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMe42XWK +HJuR7doFTX79zrEKfTlD2hjRIif3dHKJNTJNvZa52mIoHelP7RVUuFOhp7aZCNLh +IEzDyZObl8vwO6L2PVu5tbBEEoNixbpfhc8ZICEBuVo2UAhnJFcMJtuvtrCq+7ye +oczM/k/nh8FBz2WnLzWs4CZt1sa5knZXFmBmsHJQtQIC6vx7QzVcKGOlAosIEHSK +X4nIz5fLgWSzor1Gay56j31PTk+qRvlPQM2aKiLWnlLfRED4zHJqLe94itu8llPX +b6g+cLxxRKUpMqtG/15cDdBZwv40Dja7bmNfe1u4w2QCVLjvHVaVpNXbcRay/Mhn +M1w5LzDZmV58b18CAwEAAaOCAvwwggL4MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb ++ZsF4bgBjWHhMB0GA1UdDgQWBBS6/x/N38wMJrQq/cE1oIcRERMonTAOBgNVHQ8B Af8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUFBwIBFhdo dHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUFBwEBBHgw djBPBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNB RG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYX -aHR0cDovL29jc3Auc2VjdGlnby5jb20wIwYDVR0RBBwwGoIMKi52aWF0bHMuY29t -ggp2aWF0bHMuY29tMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdQDd3Mo0ldfh -FgXnlTL6x5/4PRxQ39sAOhQSdgosrLvIKgAAAZEVLi9VAAAEAwBGMEQCIGiZNOV7 -IvcHKU7nEaxFgWPpUu2CxyULg1ueJTYwTT12AiAJWQv3RrqCtOJC7JEdztILs3Bn -an9s0Bf93uOE4C/LiAB3AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw7w5CHrR+Tqo0 -AAABkRUuLxAAAAQDAEgwRgIhAOhlC+IpJV3uAaDCRXi6RZ+V8++QaLaTEtqFp2UP -yWeSAiEA8qtGDk1RE1VGwQQcJCf3CBYU5YTlsZNi7F6hEONLuzMAdwAS8U40vVNy -TIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZEVLi7kAAAEAwBIMEYCIQDWCnSm -N+2/xxo8bl3pbpNBEBZZIwnYPQW0A+SJ0+dboQIhANjH2L0xV/+rPuPMzK40vk3J -1fWHLocLjpgaxGhsBAOzMA0GCSqGSIb3DQEBCwUAA4IBAQBcObVjc1zFdOER50ZF -mI+WyVF8t6nV6dm3zIDraLA4++zKUu9UKNJm9YPqLdPP7uTLHz6wuBNmyuWPdF0r -qAf4vsK3tcAds7kjK8injewEUCPG20mtNMUHyhlNEOJR2ySPPQ6Q+t+TtGAnimKa -Zr86quYgYaJYhoEEXcbB9fMoDQYlJDzgT2DXvfM4cyoden2tYZ3gQS6ftiXacBe0 -WzFWYZ8mIP2Kb+D9tCapB9MVUzu3XJVy3S2FLQEWcWIvjnpad73a0/35i/nro6/k -TSK+MKBEBaNZuHJ8ubCToo1BftnsS8HuEPTNe8W1hyc2YmT9f5YQP6HWB2rxjH42 -OTXh +aHR0cDovL29jc3Auc2VjdGlnby5jb20wJQYDVR0RBB4wHIINKi5mYWtldGxzLmNv +bYILZmFrZXRscy5jb20wggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2AJaXZL9V +WJet90OHaDcIQnfp8DrV9qTzNm5GpD8PyqnGAAABlWsz5fgAAAQDAEcwRQIgTN7Y +/mDqiD3RbGVLEOQK2wvXsboBolBRwGJFuFEsDScCIQCQ0qfb/0V8qqSxrkx/PiVS +1lSn5gBEnQUiQOkefcnW0gB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1wQS0l +TMfUAAABlWsz5dAAAAQDAEcwRQIhAJnQJyrSCWWdi9Kyoa7XuMGyDKt183jJMY0E +71abTuBOAiBC+WnK1esG6xr8aVGHRcc+1U/I7LiaG3LCRMYtCKrTGwB2AMs49xWJ +fIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABlWsz5f4AAAQDAEcwRQIhAJUs +4PWDwyQJnCxCyEwFlFUY2uYQkGrQPA9f9Sw5Xk1fAiB63eQtZQGjvzvhOghy6z9a +8oGYbDfDQ/zfisMYO7rM6zANBgkqhkiG9w0BAQsFAAOCAQEAEHnSoeBbWiK3CS3a +px0BL+YXxRxdUcTMHgn5o+LlI9sWlpf+JLXmn7Z4QA6fAwT4k/Ue7xsmIq0OraDk +/pEVXWm1HO/9wUkGQg0DBi77BpfHircd7OWIMdt250Q8UAmZkOyhVgnwBcScqMwq +2T5CPaYvYGgYWx/qkIBv7JqhVbrP82rnF9b9ZUZ8GIE31chBmtMva9AsnAN5dmRw +81bVvPWXUfX30CYu5sxeWL06Zpy9nfJumxZri1SWXNTBjSvud2jsZ8tSCUAWLL/4 +ui3Vien9m2oMOpaA8xbS88ZTk9Alm/o5febEKJZUPlytQzij8gQpiovFw2v+Cdei ++tFXKw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB diff --git a/static/certs/key.pem b/static/certs/key.pem index 42a099a8..6135769a 100644 --- a/static/certs/key.pem +++ b/static/certs/key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE06Eo2N2lCMY4 -ISJIpCXxRMVcEhMyDNl2kDSRPksUOgnPsVEeUCazCZzP9/bB+XOYpjBFKNjp7LQi -GlTD8eWTzfh1y7L5qFQHtv8WHu3pp2eWRU1Fz8qpMw0YNIZDCYllqPzZn8qWX1Ca -M+oVZF8LIDRT01bqO2hAkBZWbJYdh0FQznwWjQcUAWjieb3+p5ZX/2i8gbWe+J5S -8p1WFfZGMyEj+HVHMUPg+B2YCMcjkH3DdxB1EtWVWiKfIbqTsoBfMxkMibsxaM84 -OSOfVPoU9ckBtxZcEwZz/rANPoVtCwU21kiEptCH9UoB5VBKsS5aLI7kxgRuOYfa -NmwM/aaNAgMBAAECggEAEYK8bzxf96tAq0SzXqAP6heSsV7AS28eN7CbpKJUnp+N -OOePDnHWB46e31exoc82DAoY+EYqiiEvY2tRSD9wi8ZCyQQOz6w8kZUju42T3/ov -Ooy+06upXYU3sIQXv8YM7bjridbv+JHRQ27D8BRGamB6l0yRinQvkbLf8d9mOYkj -P5yYrpMPV/mfgkCir/aBlGOzmI+CuOv7FdF9DIz2OehtPXOzbExuab4xOQ4NQrN9 -TfzWWS798D86e5uDx+Ab0pfege8IJvEBjU5ngZo3aeS/F5i2us+BXImu1P6IrYdb -ekXUo9VJPEHiD02iyLW/gFz3/AsWa3ztinXN0I069wKBgQD7yGPX6LG7MXlXEqL2 -DuCrFsKzVC4z0c/cpPXO8Xb8rGzvHR7Oa0i5Bo7i5wRiVXS87HT25iZmB78yjKnI -bVmWA3tVWInx6fixbZUO7L4D/Q1Ddfin/DiXyNpAhKii0QgpD61P7HJnrfnwUar5 -Vpwd2grnPNCbuILZxAZhtIXRnwKBgQDIH5hmyiIUAvrz+4UpE55ecxTMOkj0+Pgx -79KpSjXfEIk5V7UmCSk1SusQWq8Ri9d6QqPcTptVhxmC/geolp9bCW14JdORbjNv -5+3JfAwgZJtbDP4l3GKf168fLQXzSpWCW3vT1lCBz4x4nNs2EudTdDCn5aUVLGEJ -v15Iz0dQUwKBgHuZh8n55SXrx5FDCNSZwRi796Bo9rVhjhTWtgR87NhlHKTVOsZC -TFToL0Sb+776DHCh81kw6jC0JNv/yWkmpQ/LbcQbzrv/C6KuFLpa5Xy3wMcZJpPw -cSex5dI+TTqAOu1NUNsnS5IyCbw7mx8DsWfGHgweApovHa0hWbClGfwpAoGAfSt9 -6DTfkcK3cilMhX+2236BcKe4ADlFC/7jtW0sOsQeAFbCf/LU6ndchVMjEwdzlA3g -bahg8eLZaxw2cBUdwRQpey+1n83csE7RZOeIsi4bGZ0LzWSF71I5P3eqtBxfXTSZ -Q8tVeYv2YW5CkhTKyWDwGePCGHc0jqM6drHm+e8CgYEA+IkvplnK76zx3M579TpI -M2ffiC2b12FtJMpgnNrk6HZ19o2mhbte/7wf9SHVqsRw4V7/n6rzBJ5wzEHOWre7 -7PrkLgS0mY2SRfMmeT74m59dA8GpvhBUW/Xa4wlDtZkU2iIcDkKnYLjgrTKjlK86 -ER+evzmHeTqYJzuK8Mfq93I= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHuNl1ihybke3a +BU1+/c6xCn05Q9oY0SIn93RyiTUyTb2WudpiKB3pT+0VVLhToae2mQjS4SBMw8mT +m5fL8Dui9j1bubWwRBKDYsW6X4XPGSAhAblaNlAIZyRXDCbbr7awqvu8nqHMzP5P +54fBQc9lpy81rOAmbdbGuZJ2VxZgZrByULUCAur8e0M1XChjpQKLCBB0il+JyM+X +y4Fks6K9Rmsueo99T05Pqkb5T0DNmioi1p5S30RA+Mxyai3veIrbvJZT12+oPnC8 +cUSlKTKrRv9eXA3QWcL+NA42u25jX3tbuMNkAlS47x1WlaTV23EWsvzIZzNcOS8w +2ZlefG9fAgMBAAECggEAT1Tti/LASks8300b60WFxG0WMJjzGMh5eMaiSpyVtNWM +aUKJrFOjDfnhgoeUcCPWKoG/L4Sc/+EFQMydDzTte120IasysEFZ2TZytAUdcZXZ +XUMCDQNl5vCRTsJU7Q5u0t4YAGRCgMcsfTDKi8lISGiQKBHzN1CJ74Xm13rgOInd +lAc0wd5S89sL6RYmRTj1LvuZ95EHXHqQGdv0fIFEyP3pF1iPwcoTuIVEeICqnEvW +vd8CVO68eH3HFIwioqjp4qW3pxPZMhVq4161805uAMkoQlE+7MtEVenmP++1u1gM +FjvAs3j9CZqOHZKcLlOtcGSwDlD++fCMMT4slLgLgQKBgQDy58E5nuYXdxlFQQk4 +QccUKpyJ2aVXyp9xvTFBot/5Pik1SkuDzv2XU1OTxdxf3EongLy91nMJ2/6/39Je +lf0/2MjzCtJ/lSzZ/zpJAu86UkBkWBAA5loGIof6OKedbEIgqpJqtK59S+j3ExO9 +eqa+uFrtt1UfaJG4A7TT+dIvIwKBgQDSfSOdSM5Dh3KsQHVnIWcIkzwTtlJlO+rG +6rDEADxw6Kp8VIL/dq4Foe8yW4VqLVrWUuZsU6jzC9GdnyYi6VaqZ/iSUtGkBMOT +WTTYhqXlURaQ13jhqdwCZJRbVI72JbXn2OGEv8DgXnk//QKED/8VdKqAzCSr1t1f +3yfwei0AlQKBgD19KU66yKg7/+umEP1quUiDmOjUbaSRqFcUe3mQD356m9ffnMob +BdrevxNzTNv/Wc4yKpUryic+x3gu4oQLF/annAbaQHsHejkdANYmpgRvedls6XAw +360Z5K4U1WlmVD8Mrs/QOTOCmdChxad7euZgqLPwat3ujKS2W3oljW1dAoGBAM4/ +AB6lsDZLCfnuTxt2h1bHrh5CkAnR5AJ1BC+Ja6/WyvZ4eMOIroumWJKnStr3BgLr +yAxtDSbZddNUljGvIdRnfBEkRXbJlDlVN4rSpMtF4S6bcz7rCUDu/M9g05Qs70j2 +IkPJAFzZNUWVzFlKs096uXbqkSQvrUq7ho8DqAThAoGBAL7Nrbr5LWcBgvwEhEla +VRfYb0FUrDwLIrVWntJjW566/pVQQ4BmatsblLjlQYWk9MCIYXWZbnB+2fRx9yjQ +Adggez7Dws/Mrh/wVudKgayHCy5Lgd8rYjNgC+VZf8XGrWX3QXMJ6UWAyQLTeoO7 +hToW9o9CQMIhaR43G8di1kjF -----END PRIVATE KEY----- From 159598979df9abed12c28ebe7b63eed7deefeaa6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:53:40 -0700 Subject: [PATCH 673/776] fix: don't duplicate level key credits reward (#1940) Closes #1939 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1940 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index dc670c59..86c414f5 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -858,8 +858,7 @@ export const addMissionRewards = async ( for (const reward of fixedLevelRewards.levelKeyRewards2) { //quest stage completion credit rewards if (reward.rewardType == "RT_CREDITS") { - inventory.RegularCredits += reward.amount; - missionCompletionCredits += reward.amount; + missionCompletionCredits += reward.amount; // will be added to inventory in addCredits continue; } MissionRewards.push({ From f7906c91e3daed1c437b921f1229183fe949a208 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:54:04 -0700 Subject: [PATCH 674/776] fix: ignore purchaseQuantity for webui add items (#1944) Closes #1942 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1944 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/addItemsController.ts | 2 +- src/services/inventoryService.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/controllers/custom/addItemsController.ts b/src/controllers/custom/addItemsController.ts index 15837602..dc39ef64 100644 --- a/src/controllers/custom/addItemsController.ts +++ b/src/controllers/custom/addItemsController.ts @@ -7,7 +7,7 @@ export const addItemsController: RequestHandler = async (req, res) => { const requests = req.body as IAddItemRequest[]; const inventory = await getInventory(accountId); for (const request of requests) { - await addItem(inventory, request.ItemType, request.ItemCount, true); + await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, undefined, true); } await inventory.save(); res.end(); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 089e2fbb..a241e796 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -332,7 +332,8 @@ export const addItem = async ( quantity: number = 1, premiumPurchase: boolean = false, seed?: bigint, - targetFingerprint?: string + targetFingerprint?: string, + exactQuantity: boolean = false ): Promise => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { @@ -490,7 +491,9 @@ export const addItem = async ( // Multipling by purchase quantity for gear because: // - The Saya's Vigil scanner message has it as a non-counted attachment. // - Blueprints for Ancient Protector Specter, Shield Osprey Specter, etc. have num=1 despite giving their purchaseQuantity. - quantity *= ExportGear[typeName].purchaseQuantity ?? 1; + if (!exactQuantity) { + quantity *= ExportGear[typeName].purchaseQuantity ?? 1; + } const consumablesChanges = [ { ItemType: typeName, From ec9dc2aa5fe6830d7aec21a5c71986dac839b12c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:54:27 -0700 Subject: [PATCH 675/776] fix: multiply standing cost by purchase quantity (#1948) Closes #1945 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1948 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/purchaseService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 728f6570..38cfd3d1 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -223,10 +223,10 @@ export const handlePurchase = async ( purchaseResponse.Standing = [ { Tag: syndicateTag, - Standing: favour.standingCost + Standing: favour.standingCost * purchaseRequest.PurchaseParams.Quantity } ]; - affiliation.Standing -= favour.standingCost; + affiliation.Standing -= favour.standingCost * purchaseRequest.PurchaseParams.Quantity; } } } From a8227ce54cf1fb0f761f60a1a504705e4a62435f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:54:50 -0700 Subject: [PATCH 676/776] feat: cure vasca virus (#1949) Closes #1946 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1949 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index a241e796..22106441 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1509,7 +1509,9 @@ export const applyClientEquipmentUpdates = ( } if (InfestationDate) { - item.InfestationDate = fromMongoDate(InfestationDate); + // 2147483647000 means cured, otherwise became infected + item.InfestationDate = + InfestationDate.$date.$numberLong == "2147483647000" ? new Date(0) : fromMongoDate(InfestationDate); } }); }; From 8f8bc5b36430474ba76fb6972a7a27c7318d6777 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 1 May 2025 13:55:01 -0700 Subject: [PATCH 677/776] fix: don't set G3/Zanuka death marks by default (#1950) These should only be set to true after completing an invasion for the enemy faction. Re #1097 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1950 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 8c53d608..5525b500 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1688,9 +1688,9 @@ const inventorySchema = new Schema( //Like BossAladV,BossCaptainVor come for you on missions % chance DeathMarks: { type: [String], default: [] }, //Zanuka - Harvestable: { type: Boolean, default: true }, + Harvestable: Boolean, //Grustag three - DeathSquadable: { type: Boolean, default: true }, + DeathSquadable: Boolean, EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, From 35d5c01203b3e2f3bd873e55c065bdfcfbb1e3a6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 2 May 2025 04:03:42 -0700 Subject: [PATCH 678/776] fix: login failure on u29 heart of deimos and below (#1959) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1959 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 | 20 +++++++++++--------- src/types/loginTypes.ts | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 366e87c3..f872f3a2 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -110,17 +110,19 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b Groups: [], IRC: config.myIrcAddresses ?? [myAddress], NRS: config.NRS, - DTLS: 99, BuildLabel: buildLabel }; - if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { - resp.ClientType = account.ClientType; - if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { - resp.CrossPlatformAllowed = account.CrossPlatformAllowed; - resp.HUB = `https://${myAddress}/api/`; - resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; - if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { - resp.platformCDNs = [`https://${myAddress}/`]; + if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) { + resp.DTLS = 99; + if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { + resp.ClientType = account.ClientType; + if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { + resp.CrossPlatformAllowed = account.CrossPlatformAllowed; + resp.HUB = `https://${myAddress}/api/`; + resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; + if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { + resp.platformCDNs = [`https://${myAddress}/`]; + } } } } diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index dfdebe0e..3f412d1e 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -50,7 +50,7 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons { MatchmakingBuildId?: string; platformCDNs?: string[]; NRS?: string[]; - DTLS: number; + DTLS?: number; IRC: string[]; HUB?: string; } From 562ddd513fe65f1caf65974a3cf4a969ffebb81a Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Fri, 2 May 2025 15:06:36 -0700 Subject: [PATCH 679/776] chore: update docker stuff (#1961) Some Docker stuff I updated ~~but keeping WIP for now, until I know whether this breaks or not, if someone could test it for me. Will close the PR if it doesn't, cuz if I cannot even run it on my machine (Docker only crashing on my end in general), then its pointless for me to mess with it.~~ Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1961 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- Dockerfile | 45 +++++++++++++++++++++++++++++++----------- docker-compose.yml | 47 ++++++++++++++++++++++++++++++++------------ docker-entrypoint.sh | 5 +++-- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8913def2..83f9e189 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,17 +5,40 @@ ENV APP_MY_ADDRESS=localhost ENV APP_HTTP_PORT=80 ENV APP_HTTPS_PORT=443 ENV APP_AUTO_CREATE_ACCOUNT=true -ENV APP_SKIP_STORY_MODE_CHOICE=true -ENV APP_SKIP_TUTORIAL=true -ENV APP_SKIP_ALL_DIALOGUE=true -ENV APP_UNLOCK_ALL_SCANS=true -ENV APP_UNLOCK_ALL_MISSIONS=true -ENV APP_INFINITE_RESOURCES=true -ENV APP_UNLOCK_ALL_SHIP_FEATURES=true -ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true -ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true -ENV APP_UNLOCK_ALL_SKINS=true -ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true +ENV APP_SKIP_TUTORIAL=false +ENV APP_SKIP_ALL_DIALOGUE=false +ENV APP_UNLOCK_ALL_SCANS=false +ENV APP_UNLOCK_ALL_MISSIONS=false +ENV APP_INFINITE_CREDITS=false +ENV APP_INFINITE_PLATINUM=false +ENV APP_INFINITE_ENDO=false +ENV APP_INFINITE_REGAL_AYA=false +ENV APP_INFINITE_HELMINTH_MATERIALS=false +ENV APP_DONT_SUBTRACT_CONSUMABLES=false +ENV APP_UNLOCK_ALL_SHIP_FEATURES=false +ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false +ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=false +ENV APP_UNLOCK_ALL_SKINS=false +ENV APP_UNLOCK_ALL_CAPTURA_SCENES=false +ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=false +ENV APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE=false +ENV APP_UNLOCK_EXILUS_EVERYWHERE=false +ENV APP_UNLOCK_ARCANES_EVERYWHERE=false +ENV APP_NO_DAILY_FOCUS_LIMIT=false +ENV APP_NO_ARGON_CRYSTAL_DECAY=false +ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false +ENV APP_NO_VENDOR_PURCHASE_LIMITS=true +ENV APP_NO_DEATH_MARKS=false +ENV APP_NO_KIM_COOLDOWNS=false +ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false +ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false +ENV APP_SKIP_CLAN_KEY_CRAFTING=false +ENV APP_NO_DOJO_ROOM_BUILD_STAGE=false +ENV APP_NO_DECO_BUILD_STAGE=false +ENV APP_FAST_DOJO_ROOM_DESTRUCTION=false +ENV APP_NO_DOJO_RESEARCH_COSTS=false +ENV APP_NO_DOJO_RESEARCH_TIME=false +ENV APP_FAST_CLAN_ASCENSION=false ENV APP_SPOOF_MASTERY_RANK=-1 RUN apk add --no-cache bash sed wget jq diff --git a/docker-compose.yml b/docker-compose.yml index d11206cc..7b46ac1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,19 +12,40 @@ services: # APP_HTTP_PORT: 80 # APP_HTTPS_PORT: 443 # APP_AUTO_CREATE_ACCOUNT: true - # APP_SKIP_STORY_MODE_CHOICE: true - # APP_SKIP_TUTORIAL: true - # APP_SKIP_ALL_DIALOGUE: true - # APP_UNLOCK_ALL_SCANS: true - # APP_UNLOCK_ALL_MISSIONS: true - # APP_UNLOCK_ALL_QUESTS: true - # APP_COMPLETE_ALL_QUESTS: true - # APP_INFINITE_RESOURCES: true - # APP_UNLOCK_ALL_SHIP_FEATURES: true - # APP_UNLOCK_ALL_SHIP_DECORATIONS: true - # APP_UNLOCK_ALL_FLAVOUR_ITEMS: true - # APP_UNLOCK_ALL_SKINS: true - # APP_UNIVERSAL_POLARITY_EVERYWHERE: true + # APP_SKIP_TUTORIAL: false + # APP_SKIP_ALL_DIALOGUE: false + # APP_UNLOCK_ALL_SCANS: false + # APP_UNLOCK_ALL_MISSIONS: false + # APP_INFINITE_CREDITS: false + # APP_INFINITE_PLATINUM: false + # APP_INFINITE_ENDO: false + # APP_INFINITE_REGAL_AYA: false + # APP_INFINITE_HELMINTH_MATERIALS: false + # APP_DONT_SUBTRACT_CONSUMABLES: false + # APP_UNLOCK_ALL_SHIP_FEATURES: false + # APP_UNLOCK_ALL_SHIP_DECORATIONS: false + # APP_UNLOCK_ALL_FLAVOUR_ITEMS: false + # APP_UNLOCK_ALL_SKINS: false + # APP_UNLOCK_ALL_CAPTURA_SCENES: false + # APP_UNIVERSAL_POLARITY_EVERYWHERE: false + # APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE: false + # APP_UNLOCK_EXILUS_EVERYWHERE: false + # APP_UNLOCK_ARCANES_EVERYWHERE: false + # APP_NO_DAILY_FOCUS_LIMIT: false + # APP_NO_ARGON_CRYSTAL_DECAY: false + # APP_NO_MASTERY_RANK_UP_COOLDOWN: false + # APP_NO_VENDOR_PURCHASE_LIMITS: true + # APP_NO_DEATH_MARKS: false + # APP_NO_KIM_COOLDOWNS: false + # APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false + # APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false + # APP_SKIP_CLAN_KEY_CRAFTING: false + # APP_NO_DOJO_ROOM_BUILD_STAGE: false + # APP_NO_DECO_BUILD_STAGE: false + # APP_FAST_DOJO_ROOM_DESTRUCTION: false + # APP_NO_DOJO_RESEARCH_COSTS: false + # APP_NO_DOJO_RESEARCH_TIME: false + # APP_FAST_CLAN_ASCENSION: false # APP_SPOOF_MASTERY_RANK: -1 volumes: - ./docker-data/static:/app/static/data diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9ec9086b..13e70c33 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -19,5 +19,6 @@ do mv config.tmp config.json done -npm install -exec npm run dev +npm i --omit=dev +npm run build +exec npm run start From 4926b2f2bea0e40a97765566f32de24a174c0486 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 2 May 2025 15:06:46 -0700 Subject: [PATCH 680/776] fix: only refresh rewardSeed at EOM (#1957) Fixes #1953 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1957 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/missionInventoryUpdateController.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 5e642c27..41f49143 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -63,7 +63,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) missionReport.MissionStatus !== "GS_SUCCESS" && !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId) ) { - inventory.RewardSeed = generateRewardSeed(); + if (missionReport.EndOfMatchUpload) { + inventory.RewardSeed = generateRewardSeed(); + } await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); res.json({ @@ -82,7 +84,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) ConquestCompletedMissionsCount } = await addMissionRewards(inventory, missionReport, firstCompletion); - inventory.RewardSeed = generateRewardSeed(); + if (missionReport.EndOfMatchUpload) { + inventory.RewardSeed = generateRewardSeed(); + } await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); From 468efed71cac1d4a6727999c94851bd8803006d8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 2 May 2025 15:07:00 -0700 Subject: [PATCH 681/776] fix: handle tileset-specific sortie modifiers (#1958) Closes #1956 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1958 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 33 ++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 77650d0f..0ef63d74 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -298,26 +298,42 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { "SORTIE_MODIFIER_TOXIN", "SORTIE_MODIFIER_POISON", "SORTIE_MODIFIER_HAZARD_RADIATION", - "SORTIE_MODIFIER_HAZARD_MAGNETIC", - "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest - "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon - "SORTIE_MODIFIER_HAZARD_ICE", - "SORTIE_MODIFIER_HAZARD_COLD", "SORTIE_MODIFIER_SECONDARY_ONLY", "SORTIE_MODIFIER_SHOTGUN_ONLY", "SORTIE_MODIFIER_SNIPER_ONLY", "SORTIE_MODIFIER_RIFLE_ONLY", "SORTIE_MODIFIER_BOW_ONLY" ]; + const pushTilesetModifiers = (tileset: string): void => { + switch (tileset) { + case "SORTIE_MODIFIER_HAZARD_FOG": + modifiers.push("SORTIE_MODIFIER_HAZARD_FOG"); + break; + case "CorpusShipTileset": + case "GrineerGalleonTileset": + case "InfestedCorpusShipTileset": + modifiers.push("SORTIE_MODIFIER_HAZARD_MAGNETIC"); + modifiers.push("SORTIE_MODIFIER_HAZARD_FIRE"); + modifiers.push("SORTIE_MODIFIER_HAZARD_ICE"); + break; + case "CorpusIcePlanetTileset": + case "CorpusIcePlanetTilesetCaves": + modifiers.push("SORTIE_MODIFIER_HAZARD_COLD"); + break; + } + }; if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) { + const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets]; + pushTilesetModifiers(tileset); + const modifierType = rng.randomElement(modifiers)!; selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss], - tileset: sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] + tileset }); continue; } @@ -343,13 +359,16 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { modifiers.push("SORTIE_MODIFIER_SHIELDS"); } + const tileset = sortieTilesets[node as keyof typeof sortieTilesets]; + pushTilesetModifiers(tileset); + const modifierType = rng.randomElement(modifiers)!; selectedNodes.push({ missionType, modifierType, node, - tileset: sortieTilesets[node as keyof typeof sortieTilesets] + tileset }); nodes.splice(randomIndex, 1); missionTypes.add(missionType); From 8ae5fcfad0c933f7d7148d504fa51caf3c149a64 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 2 May 2025 22:14:44 -0700 Subject: [PATCH 682/776] fix: login failure on u25 & u26 (#1967) also updated the setGuildMotd response for the old UI before LongMOTD Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1967 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/getAllianceController.ts | 1 + src/controllers/api/loginController.ts | 32 +++++++++++-------- src/controllers/api/setGuildMotdController.ts | 7 +++- src/routes/api.ts | 2 ++ src/types/loginTypes.ts | 4 +-- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/getAllianceController.ts b/src/controllers/api/getAllianceController.ts index 5da0966d..a4ea7bcd 100644 --- a/src/controllers/api/getAllianceController.ts +++ b/src/controllers/api/getAllianceController.ts @@ -18,6 +18,7 @@ export const getAllianceController: RequestHandler = async (req, res) => { res.end(); }; +// POST request since U27 /*interface IGetAllianceRequest { memberCount: number; clanLeaderName: string; diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index f872f3a2..013e9067 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -101,30 +101,36 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b id: account.id, DisplayName: account.DisplayName, CountryCode: account.CountryCode, - ForceLogoutVersion: account.ForceLogoutVersion, AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, ConsentNeeded: account.ConsentNeeded, TrackedSettings: account.TrackedSettings, Nonce: account.Nonce, - Groups: [], IRC: config.myIrcAddresses ?? [myAddress], NRS: config.NRS, BuildLabel: buildLabel }; + if (version_compare(buildLabel, "2019.08.29.20.01") >= 0) { + // U25.7 and up + resp.ForceLogoutVersion = account.ForceLogoutVersion; + } + if (version_compare(buildLabel, "2019.10.31.22.42") >= 0) { + // U26 and up + resp.Groups = []; + } if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) { resp.DTLS = 99; - if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { - resp.ClientType = account.ClientType; - if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { - resp.CrossPlatformAllowed = account.CrossPlatformAllowed; - resp.HUB = `https://${myAddress}/api/`; - resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; - if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { - resp.platformCDNs = [`https://${myAddress}/`]; - } - } - } + } + if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { + resp.ClientType = account.ClientType; + } + if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { + resp.CrossPlatformAllowed = account.CrossPlatformAllowed; + resp.HUB = `https://${myAddress}/api/`; + resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; + } + if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { + resp.platformCDNs = [`https://${myAddress}/`]; } return resp; }; diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts index 8f1e28a7..62f10521 100644 --- a/src/controllers/api/setGuildMotdController.ts +++ b/src/controllers/api/setGuildMotdController.ts @@ -2,6 +2,7 @@ import { Alliance, Guild, GuildMember } from "@/src/models/guildModel"; import { hasGuildPermissionEx } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { version_compare } from "@/src/services/worldStateService"; import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; @@ -55,5 +56,9 @@ export const setGuildMotdController: RequestHandler = async (req, res) => { await guild.save(); } - res.json({ IsLongMOTD, MOTD }); + if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.03.24.20.24") > 0) { + res.json({ IsLongMOTD, MOTD }); + } else { + res.send(MOTD).end(); + } }; diff --git a/src/routes/api.ts b/src/routes/api.ts index f594c845..30616eb3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -170,6 +170,7 @@ apiRouter.get("/deleteSession.php", deleteSessionController); apiRouter.get("/divvyAllianceVault.php", divvyAllianceVaultController); apiRouter.get("/dojo", dojoController); apiRouter.get("/drones.php", dronesController); +apiRouter.get("/getAlliance.php", getAllianceController); apiRouter.get("/getDailyDealStockLevels.php", getDailyDealStockLevelsController); apiRouter.get("/getFriends.php", getFriendsController); apiRouter.get("/getGuild.php", getGuildController); @@ -308,6 +309,7 @@ apiRouter.post("/trainingResult.php", trainingResultController); apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController); apiRouter.post("/updateAlignment.php", updateAlignmentController); apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController); +apiRouter.post("/updateInventory.php", missionInventoryUpdateController); // U26 and below apiRouter.post("/updateNodeIntros.php", genericUpdateController); apiRouter.post("/updateQuest.php", updateQuestController); apiRouter.post("/updateSession.php", updateSessionPostController); diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 3f412d1e..fc4bef6a 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -5,7 +5,7 @@ export interface IAccountAndLoginResponseCommons { CountryCode: string; ClientType?: string; CrossPlatformAllowed?: boolean; - ForceLogoutVersion: number; + ForceLogoutVersion?: number; AmazonAuthToken?: string; AmazonRefreshToken?: string; ConsentNeeded: boolean; @@ -45,7 +45,7 @@ export interface ILoginRequest { export interface ILoginResponse extends IAccountAndLoginResponseCommons { id: string; - Groups: IGroup[]; + Groups?: IGroup[]; BuildLabel: string; MatchmakingBuildId?: string; platformCDNs?: string[]; From b958c108f9018c182a593dfce5fe99f749787f34 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 15:30:32 +0200 Subject: [PATCH 683/776] chore: fix typo --- 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 0ef63d74..6460752b 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -306,7 +306,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { ]; const pushTilesetModifiers = (tileset: string): void => { switch (tileset) { - case "SORTIE_MODIFIER_HAZARD_FOG": + case "GrineerForestTileset": modifiers.push("SORTIE_MODIFIER_HAZARD_FOG"); break; case "CorpusShipTileset": From 85206500185841ba77c49c38220646868ac87689 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 16:06:33 +0200 Subject: [PATCH 684/776] fix: don't push SORTIE_MODIFIER_HAZARD_RADIATION unconditionally --- src/services/worldStateService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 6460752b..397069a3 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -297,7 +297,6 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { "SORTIE_MODIFIER_FREEZE", "SORTIE_MODIFIER_TOXIN", "SORTIE_MODIFIER_POISON", - "SORTIE_MODIFIER_HAZARD_RADIATION", "SORTIE_MODIFIER_SECONDARY_ONLY", "SORTIE_MODIFIER_SHOTGUN_ONLY", "SORTIE_MODIFIER_SNIPER_ONLY", From 83743831c9f18a18d88da0bf8c9ea3a1ee3bed14 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 17:24:08 -0700 Subject: [PATCH 685/776] fix: don't divide by 0 (#1966) Closes #1964 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1966 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 86c414f5..ce9920c8 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1449,7 +1449,11 @@ function getRandomMissionDrops( } } rewardManifests = [job.rewards]; - rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)]; + if (job.xpAmounts.length > 1) { + rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)]; + } else { + rotations = [RewardInfo.JobStage!]; + } if ( RewardInfo.Q && (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && From f7b4b4f089d9fc653580147ece7b82d94702d714 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 17:24:20 -0700 Subject: [PATCH 686/776] fix: dojo time fields for old versions (#1968) Tested this in U27 & U38.5 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1968 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/guildService.ts | 7 +++++++ src/types/guildTypes.ts | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 61e64465..db6f1525 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -175,6 +175,9 @@ export const getDojoClient = async ( } if (dojoComponent.CompletionTime) { clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + clientComponent.TimeRemaining = Math.trunc( + (dojoComponent.CompletionTime.getTime() - Date.now()) / 1000 + ); if (dojoComponent.CompletionLogPending && Date.now() >= dojoComponent.CompletionTime.getTime()) { const entry = guild.RoomChanges?.find(x => x.componentId.equals(dojoComponent._id)); if (entry) { @@ -210,6 +213,9 @@ export const getDojoClient = async ( continue; } clientComponent.DestructionTime = toMongoDate(dojoComponent.DestructionTime); + clientComponent.DestructionTimeRemaining = Math.trunc( + (dojoComponent.DestructionTime.getTime() - Date.now()) / 1000 + ); } } else { clientComponent.RegularCredits = dojoComponent.RegularCredits; @@ -245,6 +251,7 @@ export const getDojoClient = async ( continue; } clientDeco.CompletionTime = toMongoDate(deco.CompletionTime); + clientDeco.TimeRemaining = Math.trunc((deco.CompletionTime.getTime() - Date.now()) / 1000); } else { clientDeco.RegularCredits = deco.RegularCredits; clientDeco.MiscItems = deco.MiscItems; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index c0e2cbe3..9dbb0d08 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -173,9 +173,11 @@ export interface IDojoComponentClient { Message?: string; RegularCredits?: number; // "Collecting Materials" state: Number of credits that were donated. MiscItems?: IMiscItem[]; // "Collecting Materials" state: Resources that were donated. - CompletionTime?: IMongoDate; + CompletionTime?: IMongoDate; // new versions + TimeRemaining?: number; // old versions RushPlatinum?: number; - DestructionTime?: IMongoDate; + DestructionTime?: IMongoDate; // new versions + DestructionTimeRemaining?: number; // old versions Decos?: IDojoDecoClient[]; DecoCapacity?: number; PaintBot?: IOid; @@ -212,7 +214,8 @@ export interface IDojoDecoClient { Sockets?: number; RegularCredits?: number; MiscItems?: IMiscItem[]; - CompletionTime?: IMongoDate; + CompletionTime?: IMongoDate; // new versions + TimeRemaining?: number; // old versions RushPlatinum?: number; PictureFrameInfo?: IPictureFrameInfo; Pending?: boolean; From 18fbd51efb820072f692137c65eddbdf3921773b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 17:24:31 -0700 Subject: [PATCH 687/776] fix: omit nightwave challenges for versions before 38.0.8 (#1969) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1969 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 397069a3..0d2d1327 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -762,15 +762,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Nightwave Challenges - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0)); - if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); - } - pushWeeklyActs(worldState, week); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { - pushWeeklyActs(worldState, week + 1); + // Current nightwave season was introduced in 38.0.8 so omitting challenges before that to avoid UI bugs and even crashes on really old versions. + if (!buildLabel || version_compare(buildLabel, "2025.02.05.11.19") >= 0) { + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0)); + if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); + } + pushWeeklyActs(worldState, week); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + pushWeeklyActs(worldState, week + 1); + } } // Elite Sanctuary Onslaught cycling every week From ff3a9b382cf9ff1bef4d6fcf001c867908bea711 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 17:24:40 -0700 Subject: [PATCH 688/776] chore: handle profile viewing data request from old versions (#1970) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1970 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../getProfileViewingDataController.ts | 129 +++++++++++------- src/routes/api.ts | 2 + src/routes/dynamic.ts | 4 +- 3 files changed, 84 insertions(+), 51 deletions(-) diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts index 766e40a2..ac818ca8 100644 --- a/src/controllers/dynamic/getProfileViewingDataController.ts +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -18,62 +18,71 @@ import { ITypeXPItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; -import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { catBreadHash, getJSONfromString } from "@/src/helpers/stringHelpers"; import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; import { IStatsClient } from "@/src/types/statTypes"; import { toStoreItem } from "@/src/services/itemDataService"; +import { FlattenMaps } from "mongoose"; -export const getProfileViewingDataController: RequestHandler = async (req, res) => { +const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise => { + const account = await Account.findById(playerId, "DisplayName"); + if (!account) { + return; + } + const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; + + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account._id), + DisplayName: account.DisplayName, + PlayerLevel: inventory.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory.XPInfo + }, + PlayerSkills: inventory.PlayerSkills, + ChallengeProgress: inventory.ChallengeProgress, + DeathMarks: inventory.DeathMarks, + Harvestable: inventory.Harvestable, + DeathSquadable: inventory.DeathSquadable, + Created: toMongoDate(inventory.Created), + MigratedToConsole: false, + Missions: inventory.Missions, + Affiliations: inventory.Affiliations, + DailyFocus: inventory.DailyFocus, + Wishlist: inventory.Wishlist, + Alignment: inventory.Alignment + }; + await populateLoadout(inventory, result); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!; + populateGuild(guild, result); + } + for (const key of allDailyAffiliationKeys) { + result[key] = inventory[key]; + } + + const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); + delete stats._id; + delete stats.__v; + delete stats.accountOwnerId; + + return { + Results: [result], + TechProjects: [], + XpComponents: [], + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: stats + }; +}; + +export const getProfileViewingDataGetController: RequestHandler = async (req, res) => { if (req.query.playerId) { - const account = await Account.findById(req.query.playerId as string, "DisplayName"); - if (!account) { + const data = await getProfileViewingDataByPlayerIdImpl(req.query.playerId as string); + if (data) { + res.json(data); + } else { res.status(409).send("Could not find requested account"); - return; } - const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; - - const result: IPlayerProfileViewingDataResult = { - AccountId: toOid(account._id), - DisplayName: account.DisplayName, - PlayerLevel: inventory.PlayerLevel, - LoadOutInventory: { - WeaponSkins: [], - XPInfo: inventory.XPInfo - }, - PlayerSkills: inventory.PlayerSkills, - ChallengeProgress: inventory.ChallengeProgress, - DeathMarks: inventory.DeathMarks, - Harvestable: inventory.Harvestable, - DeathSquadable: inventory.DeathSquadable, - Created: toMongoDate(inventory.Created), - MigratedToConsole: false, - Missions: inventory.Missions, - Affiliations: inventory.Affiliations, - DailyFocus: inventory.DailyFocus, - Wishlist: inventory.Wishlist, - Alignment: inventory.Alignment - }; - await populateLoadout(inventory, result); - if (inventory.GuildId) { - const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!; - populateGuild(guild, result); - } - for (const key of allDailyAffiliationKeys) { - result[key] = inventory[key]; - } - - const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); - delete stats._id; - delete stats.__v; - delete stats.accountOwnerId; - - res.json({ - Results: [result], - TechProjects: [], - XpComponents: [], - //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for - Stats: stats - }); } else if (req.query.guildId) { const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP"); if (!guild) { @@ -170,6 +179,28 @@ export const getProfileViewingDataController: RequestHandler = async (req, res) } }; +// For old versions, this was an authenticated POST request. +interface IGetProfileViewingDataRequest { + AccountId: string; +} +export const getProfileViewingDataPostController: RequestHandler = async (req, res) => { + const payload = getJSONfromString(String(req.body)); + const data = await getProfileViewingDataByPlayerIdImpl(payload.AccountId); + if (data) { + res.json(data); + } else { + res.status(409).send("Could not find requested account"); + } +}; + +interface IProfileViewingData { + Results: IPlayerProfileViewingDataResult[]; + TechProjects: []; + XpComponents: []; + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: FlattenMaps>; +} + interface IPlayerProfileViewingDataResult extends Partial { AccountId: IOid; DisplayName: string; diff --git a/src/routes/api.ts b/src/routes/api.ts index 30616eb3..9f5fca4b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -59,6 +59,7 @@ import { getGuildDojoController } from "@/src/controllers/api/getGuildDojoContro import { getGuildLogController } from "@/src/controllers/api/getGuildLogController"; import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController"; import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController"; +import { getProfileViewingDataPostController } from "@/src/controllers/dynamic/getProfileViewingDataController"; import { getShipController } from "@/src/controllers/api/getShipController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; @@ -247,6 +248,7 @@ apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getFriends.php", getFriendsController); apiRouter.post("/getGuildDojo.php", getGuildDojoController); +apiRouter.post("/getProfileViewingData.php", getProfileViewingDataPostController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); apiRouter.post("/gifting.php", giftingController); apiRouter.post("/gildWeapon.php", gildWeaponController); diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts index fb24fe58..c6950a16 100644 --- a/src/routes/dynamic.ts +++ b/src/routes/dynamic.ts @@ -1,14 +1,14 @@ import express from "express"; import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; import { getGuildAdsController } from "@/src/controllers/dynamic/getGuildAdsController"; -import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController"; +import { getProfileViewingDataGetController } from "@/src/controllers/dynamic/getProfileViewingDataController"; import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; const dynamicController = express.Router(); dynamicController.get("/aggregateSessions.php", aggregateSessionsController); dynamicController.get("/getGuildAds.php", getGuildAdsController); -dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController); +dynamicController.get("/getProfileViewingData.php", getProfileViewingDataGetController); dynamicController.get("/worldState.php", worldStateController); export { dynamicController }; From cad82cf7decc45777f3fc69d8437d805fe5186fc Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 17:24:47 -0700 Subject: [PATCH 689/776] fix: refuse to add horse if one is already owned (#1973) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1973 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 22106441..c4d28edc 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -789,6 +789,10 @@ export const addItem = async ( break; } case "NeutralCreatures": { + if (inventory.Horses.length != 0) { + logger.warn("refusing to add Horse because account already has one"); + return {}; + } const horseIndex = inventory.Horses.push({ ItemType: typeName }); return { Horses: [inventory.Horses[horseIndex - 1].toJSON()] From 355a70d366e2ad161ddf17b12fcb73368202e7d0 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 3 May 2025 17:48:01 -0700 Subject: [PATCH 690/776] fix: login failure on u23 and below (#1972) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1972 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 | 7 +++++-- src/types/loginTypes.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 013e9067..4ba97134 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -103,13 +103,16 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b CountryCode: account.CountryCode, AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, - ConsentNeeded: account.ConsentNeeded, - TrackedSettings: account.TrackedSettings, Nonce: account.Nonce, IRC: config.myIrcAddresses ?? [myAddress], NRS: config.NRS, BuildLabel: buildLabel }; + if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) { + // U24 and up + resp.ConsentNeeded = account.ConsentNeeded; + resp.TrackedSettings = account.TrackedSettings; + } if (version_compare(buildLabel, "2019.08.29.20.01") >= 0) { // U25.7 and up resp.ForceLogoutVersion = account.ForceLogoutVersion; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index fc4bef6a..8bbee787 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -8,8 +8,8 @@ export interface IAccountAndLoginResponseCommons { ForceLogoutVersion?: number; AmazonAuthToken?: string; AmazonRefreshToken?: string; - ConsentNeeded: boolean; - TrackedSettings: string[]; + ConsentNeeded?: boolean; + TrackedSettings?: string[]; Nonce: number; } From ec4af075b5bd26577958ce4e32e89114fc60d18f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 4 May 2025 17:30:48 -0700 Subject: [PATCH 691/776] fix: login failure on U21 (#1974) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1974 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/questControlController.ts | 25 +++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 src/controllers/api/questControlController.ts diff --git a/src/controllers/api/questControlController.ts b/src/controllers/api/questControlController.ts new file mode 100644 index 00000000..0a6e0781 --- /dev/null +++ b/src/controllers/api/questControlController.ts @@ -0,0 +1,25 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +// Basic shim handling action=sync to login on U21 +export const questControlController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const quests: IQuestState[] = []; + for (const quest of inventory.QuestKeys) { + quests.push({ + quest: quest.ItemType, + state: 3 // COMPLETE + }); + } + res.json({ + QuestState: quests + }); +}; + +interface IQuestState { + quest: string; + state: number; + task?: string; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 9f5fca4b..aeecf060 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -97,6 +97,7 @@ import { playerSkillsController } from "@/src/controllers/api/playerSkillsContro import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; +import { questControlController } from "@/src/controllers/api/questControlController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { releasePetController } from "@/src/controllers/api/releasePetController"; @@ -192,6 +193,7 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController); apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController); apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController); +apiRouter.get("/questControl.php", questControlController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); From 1914fd8f1071be2adaad31711916f02b20e93cc3 Mon Sep 17 00:00:00 2001 From: VampireKitten Date: Sun, 4 May 2025 17:31:36 -0700 Subject: [PATCH 692/776] fix: venus bounties having wrong reward tables (#1978) Due to missing / on six of the reward tables. Fixes #1977. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1978 Co-authored-by: VampireKitten Co-committed-by: VampireKitten --- src/services/worldStateService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 0d2d1327..74fa65f7 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -904,7 +904,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Jobs: [ { jobType: rng.randomElement(venusJobs), - rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, masteryReq: 0, minEnemyLevel: 5, maxEnemyLevel: 15, @@ -912,7 +912,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }, { jobType: rng.randomElement(venusJobs), - rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, masteryReq: 1, minEnemyLevel: 10, maxEnemyLevel: 30, @@ -920,7 +920,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }, { jobType: rng.randomElement(venusJobs), - rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, masteryReq: 2, minEnemyLevel: 20, maxEnemyLevel: 40, @@ -928,7 +928,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }, { jobType: rng.randomElement(venusJobs), - rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, masteryReq: 3, minEnemyLevel: 30, maxEnemyLevel: 50, @@ -936,7 +936,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }, { jobType: rng.randomElement(venusJobs), - rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, masteryReq: 5, minEnemyLevel: 40, maxEnemyLevel: 60, @@ -944,7 +944,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }, { jobType: rng.randomElement(venusJobs), - rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, masteryReq: 10, minEnemyLevel: 100, maxEnemyLevel: 100, From 238af294fe8eb843d8aebbc2461b9c9596343fab Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 5 May 2025 04:57:44 -0700 Subject: [PATCH 693/776] fix: login failure on U18 (#1983) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1983 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/app.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app.ts b/src/app.ts index 291372f0..36c1437e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,6 +21,12 @@ app.use((req, _res, next) => { if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { req.headers["content-encoding"] = undefined; } + + // U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like. + if (req.headers["content-type"] == "application/x-www-form-urlencoded") { + req.headers["content-type"] = "application/octet-stream"; + } + next(); }); From cfa3586f644d115b05e6a5181e5be8e3e5b9e9c6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 5 May 2025 18:03:36 -0700 Subject: [PATCH 694/776] fix: don't provide syndicate missions in advance (#1979) Closes #1975 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1979 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 74fa65f7..63ff7a82 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -193,6 +193,12 @@ const pushSyndicateMissions = ( idSuffix: string, syndicateTag: string ): void => { + const dayStart = getSortieTime(day); + if (Date.now() >= dayStart) { + return; // The client does not seem to respect activation. + } + const dayEnd = getSortieTime(day + 1); + const nodeOptions: string[] = [...syndicateMissions]; const rng = new CRng(seed); @@ -203,8 +209,6 @@ const pushSyndicateMissions = ( nodeOptions.splice(index, 1); } - const dayStart = getSortieTime(day); - const dayEnd = getSortieTime(day + 1); worldState.SyndicateMissions.push({ _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix }, Activation: { $date: { $numberLong: dayStart.toString() } }, From c53dc2fd02b2f8247dd662cf65686ab6226b253c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 5 May 2025 18:03:44 -0700 Subject: [PATCH 695/776] fix: remove non-existent sortie modifiers (#1980) Closes #1976 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1980 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, 2 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 63ff7a82..946997fc 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -295,11 +295,9 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { "SORTIE_MODIFIER_VIRAL", "SORTIE_MODIFIER_ELECTRICITY", "SORTIE_MODIFIER_RADIATION", - "SORTIE_MODIFIER_GAS", "SORTIE_MODIFIER_FIRE", "SORTIE_MODIFIER_EXPLOSION", "SORTIE_MODIFIER_FREEZE", - "SORTIE_MODIFIER_TOXIN", "SORTIE_MODIFIER_POISON", "SORTIE_MODIFIER_SECONDARY_ONLY", "SORTIE_MODIFIER_SHOTGUN_ONLY", From e2fe4060176148ae8b153a817a2a5464b3cc5c21 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 5 May 2025 18:03:53 -0700 Subject: [PATCH 696/776] fix: always use rotation A for Profit-Taker bounty rewards (#1981) Closes #1964 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1981 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index ce9920c8..9a8d770b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1452,7 +1452,7 @@ function getRandomMissionDrops( if (job.xpAmounts.length > 1) { rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)]; } else { - rotations = [RewardInfo.JobStage!]; + rotations = [0]; } if ( RewardInfo.Q && From 4e57bcd1ae55a8f18c99b5b8f8411328acab80d5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 5 May 2025 18:09:03 -0700 Subject: [PATCH 697/776] fix: login failure on U17 (#1986) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1986 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/app.ts | 3 ++- src/controllers/api/loginController.ts | 7 +++++-- src/routes/api.ts | 2 ++ src/services/worldStateService.ts | 4 ++++ src/types/worldStateTypes.ts | 16 ++++++++++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/app.ts b/src/app.ts index 36c1437e..48079166 100644 --- a/src/app.ts +++ b/src/app.ts @@ -23,7 +23,8 @@ app.use((req, _res, next) => { } // U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like. - if (req.headers["content-type"] == "application/x-www-form-urlencoded") { + // U17 sets no Content-Type at all, which Express also doesn't like. + if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") { req.headers["content-type"] = "application/octet-stream"; } diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 4ba97134..de8b7951 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -82,8 +82,11 @@ export const loginController: RequestHandler = async (request, response) => { } } else { if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) { - response.status(400).json({ error: "nonce still set" }); - return; + // U17 seems to handle "nonce still set" like a login failure. + if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) { + response.status(400).send({ error: "nonce still set" }); + return; + } } account.ClientType = loginRequest.ClientType; diff --git a/src/routes/api.ts b/src/routes/api.ts index aeecf060..56602f4a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -186,6 +186,7 @@ apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/hub", hubController); apiRouter.get("/hubInstances", hubInstancesController); apiRouter.get("/inbox.php", inboxController); +apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/inventory.php", inventoryController); apiRouter.get("/loginRewards.php", loginRewardsController); apiRouter.get("/logout.php", logoutController); @@ -278,6 +279,7 @@ apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); +apiRouter.post("/questControl.php", questControlController); // U17 apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 946997fc..a8129194 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -744,6 +744,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // Omit void fissures for versions prior to Dante Unbound to avoid script errors. if (buildLabel && version_compare(buildLabel, "2024.03.24.20.00") < 0) { worldState.ActiveMissions = []; + if (version_compare(buildLabel, "2017.10.12.17.04") < 0) { + // Old versions seem to really get hung up on not being able to load these. + worldState.PVPChallengeInstances = []; + } } if (config.worldState?.starDays) { diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 1e8d4033..68c68753 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -12,6 +12,7 @@ export interface IWorldState { GlobalUpgrades: IGlobalUpgrade[]; ActiveMissions: IFissure[]; NodeOverrides: INodeOverride[]; + PVPChallengeInstances: IPVPChallengeInstance[]; EndlessXpChoices: IEndlessXpChoice[]; SeasonInfo: { Activation: IMongoDate; @@ -130,6 +131,21 @@ export interface ILiteSortie { }[]; } +export interface IPVPChallengeInstance { + _id: IOid; + challengeTypeRefID: string; + startDate: IMongoDate; + endDate: IMongoDate; + params: { + n: string; // "ScriptParamValue"; + v: number; + }[]; + isGenerated: boolean; + PVPMode: string; + subChallenges: IOid[]; + Category: string; // "PVPChallengeTypeCategory_WEEKLY" | "PVPChallengeTypeCategory_WEEKLY_ROOT" | "PVPChallengeTypeCategory_DAILY"; +} + export interface IEndlessXpChoice { Category: string; Choices: string[]; From 460deed3ed2631363ca2185c1e98e0f0c8730752 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 02:29:03 -0700 Subject: [PATCH 698/776] fix: login failure on U16 (#1991) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1991 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 | 9 ++++++--- src/types/loginTypes.ts | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index de8b7951..8f368a67 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -41,7 +41,7 @@ export const loginController: RequestHandler = async (request, response) => { email: loginRequest.email, password: loginRequest.password, DisplayName: name, - CountryCode: loginRequest.lang.toUpperCase(), + CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, CrossPlatformAllowed: true, ForceLogoutVersion: 0, @@ -91,7 +91,7 @@ export const loginController: RequestHandler = async (request, response) => { account.ClientType = loginRequest.ClientType; account.Nonce = nonce; - account.CountryCode = loginRequest.lang.toUpperCase(); + account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN"; account.BuildLabel = buildLabel; } await account.save(); @@ -107,10 +107,13 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, Nonce: account.Nonce, - IRC: config.myIrcAddresses ?? [myAddress], NRS: config.NRS, BuildLabel: buildLabel }; + if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) { + // U17 and up + resp.IRC = config.myIrcAddresses ?? [myAddress]; + } if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) { // U24 and up resp.ConsentNeeded = account.ConsentNeeded; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 8bbee787..5a7f9e46 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -35,11 +35,11 @@ export interface ILoginRequest { email: string; password: string; time: number; - s: string; - lang: string; + s?: string; + lang?: string; date: number; - ClientType: string; - PS: string; + ClientType?: string; + PS?: string; kick?: boolean; } @@ -51,7 +51,7 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons { platformCDNs?: string[]; NRS?: string[]; DTLS?: number; - IRC: string[]; + IRC?: string[]; HUB?: string; } From da6d75c74897f1ac8f21e2772bb94d766c839635 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 02:29:11 -0700 Subject: [PATCH 699/776] fix: don't give sortie assassination rewards if mission type differs (#1992) Closes #1987 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1992 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 21 ++++++- src/services/worldStateService.ts | 60 ++++++++++--------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 9a8d770b..df7c6121 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -58,7 +58,7 @@ import conservationAnimals from "@/static/fixed_responses/conservationAnimals.js import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; +import { getLiteSortie, getSortie, getWorldState, idToDay, idToWeek } from "./worldStateService"; import { config } from "./configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; @@ -1357,9 +1357,24 @@ function getRandomMissionDrops( // Invasion assassination has Phorid has the boss who should drop Nyx parts // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"]; - } else if (RewardInfo.sortieId && region.missionIndex != 0) { + } else if (RewardInfo.sortieId) { // Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. Assassinations are an exception to this. - rewardManifests = []; + if (region.missionIndex == 0) { + const arr = RewardInfo.sortieId.split("_"); + let sortieId = arr[1]; + if (sortieId == "Lite") { + sortieId = arr[2]; + } + const sortie = getSortie(idToDay(sortieId)); + const mission = sortie.Variants.find(x => x.node == arr[0])!; + if (mission.missionType == "MT_ASSASSINATION") { + rewardManifests = region.rewardManifests; + } else { + rewardManifests = []; + } + } else { + rewardManifests = []; + } } else { rewardManifests = region.rewardManifests; } diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index a8129194..669824ab 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -12,6 +12,7 @@ import { ICalendarSeason, ILiteSortie, ISeasonChallenge, + ISortie, ISortieMission, IWorldState } from "../types/worldStateTypes"; @@ -193,12 +194,6 @@ const pushSyndicateMissions = ( idSuffix: string, syndicateTag: string ): void => { - const dayStart = getSortieTime(day); - if (Date.now() >= dayStart) { - return; // The client does not seem to respect activation. - } - const dayEnd = getSortieTime(day + 1); - const nodeOptions: string[] = [...syndicateMissions]; const rng = new CRng(seed); @@ -209,6 +204,8 @@ const pushSyndicateMissions = ( nodeOptions.splice(index, 1); } + const dayStart = getSortieTime(day); + const dayEnd = getSortieTime(day + 1); worldState.SyndicateMissions.push({ _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix }, Activation: { $date: { $numberLong: dayStart.toString() } }, @@ -219,16 +216,7 @@ const pushSyndicateMissions = ( }); }; -const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { - const dayStart = getSortieTime(day); - if (!isBeforeNextExpectedWorldStateRefresh(dayStart)) { - return; - } - const dayEnd = getSortieTime(day + 1); - if (Date.now() >= dayEnd) { - return; - } - +export const getSortie = (day: number): ISortie => { const seed = new CRng(day).randomInt(0, 0xffff); const rng = new CRng(seed); @@ -375,7 +363,9 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { missionTypes.add(missionType); } - worldState.Sorties.push({ + const dayStart = getSortieTime(day); + const dayEnd = getSortieTime(day + 1); + return { _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "d4d932c97c0a3acd" }, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, @@ -383,14 +373,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => { Seed: seed, Boss: boss, Variants: selectedNodes - }); - - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate"); + }; }; const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => @@ -1099,8 +1082,25 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST) - pushSortieIfRelevant(worldState, day - 1); - pushSortieIfRelevant(worldState, day); + { + const rollover = getSortieTime(day); + + if (Date.now() < rollover) { + worldState.Sorties.push(getSortie(day - 1)); + } + if (isBeforeNextExpectedWorldStateRefresh(rollover)) { + worldState.Sorties.push(getSortie(day)); + } + + // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. + const rng = new CRng(Date.now() >= rollover ? day : day - 1); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate"); + pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate"); + } // Archon Hunt cycling every week worldState.LiteSorties.push(getLiteSortie(week)); @@ -1178,8 +1178,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => { return worldState; }; +export const idToDay = (id: string): number => { + return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 86400_000); +}; + export const idToWeek = (id: string): number => { - return (parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800000; + return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800_000); }; export const getLiteSortie = (week: number): ILiteSortie => { From f6cbc02c47c392fc4df0e8e38e963ff96cb1ab0d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 02:29:23 -0700 Subject: [PATCH 700/776] fix: ignore client providing not-an-id in recipe ids array (#1990) Closes #1989 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1990 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/startRecipeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index 495b8d26..78adf1fc 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -41,7 +41,7 @@ export const startRecipeController: RequestHandler = async (req, res) => { ]; for (let i = 0; i != recipe.ingredients.length; ++i) { - if (startRecipeRequest.Ids[i]) { + if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; if (category != "LongGuns" && category != "Pistols" && category != "Melee") { throw new Error(`unexpected equipment ingredient type: ${category}`); From 5fefd189af001e4c3b09c5776d7dde6b6fb518e2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 06:05:12 -0700 Subject: [PATCH 701/776] fix: login failure on U15 (#1997) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1997 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 8f368a67..07a9b9da 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -107,9 +107,11 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, Nonce: account.Nonce, - NRS: config.NRS, BuildLabel: buildLabel }; + if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) { + resp.NRS = config.NRS; + } if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) { // U17 and up resp.IRC = config.myIrcAddresses ?? [myAddress]; From 005690daa4d065fccd8d7a8e758c93382a1dc184 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 07:37:30 -0700 Subject: [PATCH 702/776] fix: handle "skip prologue" to visit orbiter in U14 (#1999) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1999 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 | 9 +++++++++ src/routes/api.ts | 1 + src/types/inventoryTypes/inventoryTypes.ts | 2 ++ 3 files changed, 12 insertions(+) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 8febdcf7..10d1364a 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -25,6 +25,9 @@ import { logger } from "@/src/utils/logger"; import { catBreadHash } from "@/src/helpers/stringHelpers"; import { Types } from "mongoose"; import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers"; +import { version_compare } from "@/src/services/worldStateService"; +import { getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; export const inventoryController: RequestHandler = async (request, response) => { const account = await getAccountForRequest(request); @@ -312,6 +315,12 @@ export const getInventoryResponse = async ( inventoryResponse.Nemesis = undefined; } + if (buildLabel && version_compare(buildLabel, "2018.02.22.14.34") < 0) { + const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString()); + const personalRooms = personalRoomsDb.toJSON(); + inventoryResponse.Ship = personalRooms.Ship; + } + return inventoryResponse; }; diff --git a/src/routes/api.ts b/src/routes/api.ts index 56602f4a..787164d9 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -225,6 +225,7 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController); +apiRouter.post("/commitStoryModeDecision.php", (_req, res) => { res.end(); }); // U14 (maybe wanna actually unlock the ship features?) apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationPostController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 92b6973e..23b8ec38 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -11,6 +11,7 @@ import { IOperatorConfigDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper"; +import { IOrbiter } from "../personalRoomsTypes"; export type InventoryDatabaseEquipment = { [_ in TEquipmentKey]: IEquipmentDatabase[]; @@ -373,6 +374,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu BrandedSuits?: IOid[]; LockedWeaponGroup?: ILockedWeaponGroupClient; HubNpcCustomizations?: IHubNpcCustomization[]; + Ship?: IOrbiter; // U22 and below, response only } export interface IAffiliation { From 203b3e20d9d94b4edeb244b355627e240aabe596 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 19:04:22 -0700 Subject: [PATCH 703/776] fix: refuse to give starting gear again (#1993) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1993 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index c4d28edc..6a30df38 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -149,6 +149,11 @@ export const addStartingGear = async ( inventory: TInventoryDatabaseDocument, startingGear?: TPartialStartingGear ): Promise => { + if (inventory.ReceivedStartingGear) { + throw new Error(`account has already received starting gear`); + } + inventory.ReceivedStartingGear = true; + const { LongGuns, Pistols, Suits, Melee } = startingGear || { LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], @@ -197,11 +202,6 @@ export const addStartingGear = async ( combineInventoryChanges(inventoryChanges, inventoryDelta); } - if (inventory.ReceivedStartingGear) { - logger.warn(`account already had starting gear but asked for it again?!`); - } - inventory.ReceivedStartingGear = true; - return inventoryChanges; }; From 4f28688837c3e584ea938e605e2a752f4f4d7647 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 19:04:39 -0700 Subject: [PATCH 704/776] feat: add CompletedVorsPrize to getAccountInfo response (#1994) Closes #1941 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1994 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/getAccountInfoController.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/controllers/custom/getAccountInfoController.ts b/src/controllers/custom/getAccountInfoController.ts index 0f6524ad..668aebf4 100644 --- a/src/controllers/custom/getAccountInfoController.ts +++ b/src/controllers/custom/getAccountInfoController.ts @@ -1,15 +1,18 @@ import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; import { RequestHandler } from "express"; export const getAccountInfoController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "QuestKeys"); const info: IAccountInfo = { - DisplayName: account.DisplayName + DisplayName: account.DisplayName, + IsAdministrator: isAdministrator(account), + CompletedVorsPrize: !!inventory.QuestKeys.find( + x => x.ItemType == "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" + )?.Completed }; - if (isAdministrator(account)) { - info.IsAdministrator = true; - } const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); if (guildMember) { const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!; @@ -31,7 +34,8 @@ export const getAccountInfoController: RequestHandler = async (req, res) => { interface IAccountInfo { DisplayName: string; - IsAdministrator?: boolean; + IsAdministrator: boolean; + CompletedVorsPrize: boolean; GuildId?: string; GuildPermissions?: number; GuildRank?: number; From 4b12fe12cb58b8784bd78122ee27f99bd4e4c546 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 19:04:53 -0700 Subject: [PATCH 705/776] feat: handle mechsuits in sellController (#1996) Closes #1995 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1996 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/sellController.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index 8e9d7e08..fdfb3a82 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -45,6 +45,9 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) { requiredFields.add(InventorySlot.SPACEWEAPONS); } + if (payload.Items.MechSuits) { + requiredFields.add(InventorySlot.MECHSUITS); + } if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) { requiredFields.add(InventorySlot.SENTINELS); } @@ -136,6 +139,12 @@ export const sellController: RequestHandler = async (req, res) => { freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); }); } + if (payload.Items.MechSuits) { + payload.Items.MechSuits.forEach(sellItem => { + inventory.MechSuits.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.MECHSUITS); + }); + } if (payload.Items.Sentinels) { payload.Items.Sentinels.forEach(sellItem => { inventory.Sentinels.pull({ _id: sellItem.String }); @@ -285,6 +294,7 @@ interface ISellRequest { SpaceSuits?: ISellItem[]; SpaceGuns?: ISellItem[]; SpaceMelee?: ISellItem[]; + MechSuits?: ISellItem[]; Sentinels?: ISellItem[]; SentinelWeapons?: ISellItem[]; MoaPets?: ISellItem[]; From bb606f3a95742b956a5f9651e6077bb1d6f49d3c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 6 May 2025 19:05:23 -0700 Subject: [PATCH 706/776] fix: get bounty info by id to handle rollover (#1998) Closes #1988 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1998 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 22 +- src/services/worldStateService.ts | 497 +++++++++--------- 2 files changed, 265 insertions(+), 254 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index df7c6121..1344244d 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -58,9 +58,10 @@ import conservationAnimals from "@/static/fixed_responses/conservationAnimals.js import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -import { getLiteSortie, getSortie, getWorldState, idToDay, idToWeek } from "./worldStateService"; +import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService"; import { config } from "./configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; +import { ISyndicateMissionInfo } from "../types/worldStateTypes"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C @@ -1086,10 +1087,10 @@ export const addMissionRewards = async ( if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [jobType, unkIndex, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_"); - const worldState = getWorldState(); - let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId); - if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag + const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_"); + const syndicateMissions: ISyndicateMissionInfo[] = []; + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry && syndicateEntry.Jobs) { let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; if (syndicateEntry.Tag === "EntratiSyndicate") { @@ -1383,13 +1384,12 @@ function getRandomMissionDrops( if (RewardInfo.jobId) { if (RewardInfo.JobStage! >= 0) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [jobType, unkIndex, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); + const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = RewardInfo.jobId.split("_"); let isEndlessJob = false; - if (syndicateId) { - const worldState = getWorldState(); - let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId); - if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); - + if (syndicateMissionId) { + const syndicateMissions: ISyndicateMissionInfo[] = []; + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry && syndicateEntry.Jobs) { let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 669824ab..60a64d83 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -14,6 +14,7 @@ import { ISeasonChallenge, ISortie, ISortieMission, + ISyndicateMissionInfo, IWorldState } from "../types/worldStateTypes"; @@ -457,6 +458,254 @@ const pushWeeklyActs = (worldState: IWorldState, week: number): void => { }); }; +export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => { + const table = String.fromCharCode(65 + (bountyCycle % 3)); + const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); + const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); + + // TODO: xpAmounts need to be calculated based on the jobType somehow? + + const seed = new CRng(bountyCycle).randomInt(0, 0xffff); + const bountyCycleStart = bountyCycle * 9000000; + const bountyCycleEnd = bountyCycleStart + 9000000; + + { + const rng = new CRng(seed); + syndicateMissions.push({ + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "CetusSyndicate", + Seed: seed, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [430, 430, 430] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [620, 620, 620] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [670, 670, 670, 990] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [570, 570, 570, 570, 1110] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [740, 740, 740, 740, 1450] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(eidolonNarmerJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [840, 840, 840, 840, 1650] + } + ] + }); + } + + { + const rng = new CRng(seed); + syndicateMissions.push({ + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025" + }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "SolarisSyndicate", + Seed: seed, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [340, 340, 340] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [660, 660, 660] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [610, 610, 610, 900] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [600, 600, 600, 600, 1170] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [690, 690, 690, 690, 1350] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(venusNarmerJobs), + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [780, 780, 780, 780, 1540] + } + ] + }); + } + + { + const rng = new CRng(seed); + syndicateMissions.push({ + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002" + }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "EntratiSyndicate", + Seed: seed, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [5, 5, 5] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [12, 12, 12] + }, + { + jobType: rng.randomElement(microplanetEndlessJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 25, + maxEnemyLevel: 30, + endless: true, + xpAmounts: [14, 14, 14] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`, + masteryReq: 2, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [17, 17, 17, 25] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [22, 22, 22, 22, 43] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [25, 25, 25, 25, 50] + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [2, 2, 2, 4], + locationTag: "ChamberB", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [4, 4, 4, 5], + locationTag: "ChamberA", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 50, + maxEnemyLevel: 60, + xpAmounts: [5, 5, 5, 7], + locationTag: "ChamberC", + isVault: true + } + ] + }); + } +}; + const birthdays: number[] = [ 1, // Kaya 45, // Lettie @@ -799,249 +1048,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Nodes: [] }); - const table = String.fromCharCode(65 + (bountyCycle % 3)); - const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); - const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); - - // TODO: xpAmounts need to be calculated based on the jobType somehow? - - const seed = new CRng(bountyCycle).randomInt(0, 0xffff); - - { - const rng = new CRng(seed); - worldState.SyndicateMissions.push({ - _id: { - $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" - }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "CetusSyndicate", - Seed: seed, - Nodes: [], - Jobs: [ - { - jobType: rng.randomElement(eidolonJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`, - masteryReq: 0, - minEnemyLevel: 5, - maxEnemyLevel: 15, - xpAmounts: [430, 430, 430] - }, - { - jobType: rng.randomElement(eidolonJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`, - masteryReq: 1, - minEnemyLevel: 10, - maxEnemyLevel: 30, - xpAmounts: [620, 620, 620] - }, - { - jobType: rng.randomElement(eidolonJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`, - masteryReq: 2, - minEnemyLevel: 20, - maxEnemyLevel: 40, - xpAmounts: [670, 670, 670, 990] - }, - { - jobType: rng.randomElement(eidolonJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`, - masteryReq: 3, - minEnemyLevel: 30, - maxEnemyLevel: 50, - xpAmounts: [570, 570, 570, 570, 1110] - }, - { - jobType: rng.randomElement(eidolonJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, - masteryReq: 5, - minEnemyLevel: 40, - maxEnemyLevel: 60, - xpAmounts: [740, 740, 740, 740, 1450] - }, - { - jobType: rng.randomElement(eidolonJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, - masteryReq: 10, - minEnemyLevel: 100, - maxEnemyLevel: 100, - xpAmounts: [840, 840, 840, 840, 1660] - }, - { - jobType: rng.randomElement(eidolonNarmerJobs), - rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`, - masteryReq: 0, - minEnemyLevel: 50, - maxEnemyLevel: 70, - xpAmounts: [840, 840, 840, 840, 1650] - } - ] - }); - } - - { - const rng = new CRng(seed); - worldState.SyndicateMissions.push({ - _id: { - $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025" - }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "SolarisSyndicate", - Seed: seed, - Nodes: [], - Jobs: [ - { - jobType: rng.randomElement(venusJobs), - rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, - masteryReq: 0, - minEnemyLevel: 5, - maxEnemyLevel: 15, - xpAmounts: [340, 340, 340] - }, - { - jobType: rng.randomElement(venusJobs), - rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, - masteryReq: 1, - minEnemyLevel: 10, - maxEnemyLevel: 30, - xpAmounts: [660, 660, 660] - }, - { - jobType: rng.randomElement(venusJobs), - rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, - masteryReq: 2, - minEnemyLevel: 20, - maxEnemyLevel: 40, - xpAmounts: [610, 610, 610, 900] - }, - { - jobType: rng.randomElement(venusJobs), - rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, - masteryReq: 3, - minEnemyLevel: 30, - maxEnemyLevel: 50, - xpAmounts: [600, 600, 600, 600, 1170] - }, - { - jobType: rng.randomElement(venusJobs), - rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, - masteryReq: 5, - minEnemyLevel: 40, - maxEnemyLevel: 60, - xpAmounts: [690, 690, 690, 690, 1350] - }, - { - jobType: rng.randomElement(venusJobs), - rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, - masteryReq: 10, - minEnemyLevel: 100, - maxEnemyLevel: 100, - xpAmounts: [840, 840, 840, 840, 1660] - }, - { - jobType: rng.randomElement(venusNarmerJobs), - rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - masteryReq: 0, - minEnemyLevel: 50, - maxEnemyLevel: 70, - xpAmounts: [780, 780, 780, 780, 1540] - } - ] - }); - } - - { - const rng = new CRng(seed); - worldState.SyndicateMissions.push({ - _id: { - $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002" - }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "EntratiSyndicate", - Seed: seed, - Nodes: [], - Jobs: [ - { - jobType: rng.randomElement(microplanetJobs), - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`, - masteryReq: 0, - minEnemyLevel: 5, - maxEnemyLevel: 15, - xpAmounts: [5, 5, 5] - }, - { - jobType: rng.randomElement(microplanetJobs), - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`, - masteryReq: 1, - minEnemyLevel: 15, - maxEnemyLevel: 25, - xpAmounts: [12, 12, 12] - }, - { - jobType: rng.randomElement(microplanetEndlessJobs), - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`, - masteryReq: 5, - minEnemyLevel: 25, - maxEnemyLevel: 30, - endless: true, - xpAmounts: [14, 14, 14] - }, - { - jobType: rng.randomElement(microplanetJobs), - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`, - masteryReq: 2, - minEnemyLevel: 30, - maxEnemyLevel: 40, - xpAmounts: [17, 17, 17, 25] - }, - { - jobType: rng.randomElement(microplanetJobs), - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, - masteryReq: 3, - minEnemyLevel: 40, - maxEnemyLevel: 60, - xpAmounts: [22, 22, 22, 22, 43] - }, - { - jobType: rng.randomElement(microplanetJobs), - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, - masteryReq: 10, - minEnemyLevel: 100, - maxEnemyLevel: 100, - xpAmounts: [25, 25, 25, 25, 50] - }, - { - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`, - masteryReq: 5, - minEnemyLevel: 30, - maxEnemyLevel: 40, - xpAmounts: [2, 2, 2, 4], - locationTag: "ChamberB", - isVault: true - }, - { - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`, - masteryReq: 5, - minEnemyLevel: 40, - maxEnemyLevel: 50, - xpAmounts: [4, 4, 4, 5], - locationTag: "ChamberA", - isVault: true - }, - { - rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`, - masteryReq: 5, - minEnemyLevel: 50, - maxEnemyLevel: 60, - xpAmounts: [5, 5, 5, 7], - locationTag: "ChamberC", - isVault: true - } - ] - }); - } + pushClassicBounties(worldState.SyndicateMissions, bountyCycle); } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { @@ -1178,6 +1185,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { return worldState; }; +export const idToBountyCycle = (id: string): number => { + return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000); +}; + export const idToDay = (id: string): number => { return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 86400_000); }; From 58549c1488142eb380077c10fe140adcb0825129 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 7 May 2025 20:13:45 -0700 Subject: [PATCH 707/776] fix: exclude railjack missions from archon hunt (#2002) Closes #2001 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2002 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 60a64d83..a9535c9f 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1209,7 +1209,8 @@ export const getLiteSortie = (week: number): ILiteSortie => { value.factionIndex !== undefined && value.factionIndex < 2 && !isArchwingMission(value) && - value.missionIndex != 0 // Exclude MT_ASSASSINATION + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 32 // Exclude railjack ) { nodes.push(key); } From e545ecf7675e10c379dadbe2ab7a022f9879a699 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 7 May 2025 20:14:05 -0700 Subject: [PATCH 708/776] fix: restrict sortie mission types based on what the tileset supports (#2003) Closes #2000 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2003 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 98 ++++++------------- .../worldState/sortieTilesetMissions.json | 45 +++++++++ 2 files changed, 77 insertions(+), 66 deletions(-) create mode 100644 static/fixed_responses/worldState/sortieTilesetMissions.json diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index a9535c9f..09b1c483 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1,11 +1,12 @@ import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json"; +import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; -import { eMissionType, ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; +import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, ICalendarEvent, @@ -72,12 +73,6 @@ const sortieFactionToFactionIndexes: Record = { FC_OROKIN: [3] }; -const sortieFactionToSpecialMissionTileset: Record = { - FC_GRINEER: "GrineerGalleonTileset", - FC_CORPUS: "CorpusShipTileset", - FC_INFESTATION: "CorpusShipTileset" -}; - const sortieBossNode: Record, string> = { SORTIE_BOSS_ALAD: "SolNode53", SORTIE_BOSS_AMBULAS: "SolNode51", @@ -217,6 +212,27 @@ const pushSyndicateMissions = ( }); }; +type TSortieTileset = keyof typeof sortieTilesetMissions; + +const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): void => { + switch (tileset) { + case "GrineerForestTileset": + modifiers.push("SORTIE_MODIFIER_HAZARD_FOG"); + break; + case "CorpusShipTileset": + case "GrineerGalleonTileset": + case "InfestedCorpusShipTileset": + modifiers.push("SORTIE_MODIFIER_HAZARD_MAGNETIC"); + modifiers.push("SORTIE_MODIFIER_HAZARD_FIRE"); + modifiers.push("SORTIE_MODIFIER_HAZARD_ICE"); + break; + case "CorpusIcePlanetTileset": + case "CorpusIcePlanetTilesetCaves": + modifiers.push("SORTIE_MODIFIER_HAZARD_COLD"); + break; + } +}; + export const getSortie = (day: number): ISortie => { const seed = new CRng(day).randomInt(0, 0xffff); const rng = new CRng(seed); @@ -224,54 +240,22 @@ export const getSortie = (day: number): ISortie => { const boss = rng.randomElement(sortieBosses)!; const nodes: string[] = []; - const availableMissionIndexes: number[] = []; for (const [key, value] of Object.entries(ExportRegions)) { if ( sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && key in sortieTilesets ) { - if ( - value.missionIndex != 0 && // Assassination will be decided independently - value.missionIndex != 5 && // Sorties do not have capture missions - !availableMissionIndexes.includes(value.missionIndex) - ) { - availableMissionIndexes.push(value.missionIndex); - } nodes.push(key); } } - const specialMissionTypes = [1, 3, 5, 9]; - if (!(sortieBossToFaction[boss] in sortieFactionToSpecialMissionTileset)) { - for (const missionType of specialMissionTypes) { - const i = availableMissionIndexes.indexOf(missionType); - if (i != -1) { - availableMissionIndexes.splice(i, 1); - } - } - } - const selectedNodes: ISortieMission[] = []; const missionTypes = new Set(); for (let i = 0; i < 3; i++) { - let randomIndex; - let node; - let missionIndex; - do { - randomIndex = rng.randomInt(0, nodes.length - 1); - node = nodes[randomIndex]; - - missionIndex = ExportRegions[node].missionIndex; - if (missionIndex != 28) { - missionIndex = rng.randomElement(availableMissionIndexes)!; - } - } while ( - specialMissionTypes.indexOf(missionIndex) != -1 && - sortieTilesets[node as keyof typeof sortieTilesets] != - sortieFactionToSpecialMissionTileset[sortieBossToFaction[boss]] - ); + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; const modifiers = [ "SORTIE_MODIFIER_LOW_ENERGY", @@ -294,28 +278,10 @@ export const getSortie = (day: number): ISortie => { "SORTIE_MODIFIER_RIFLE_ONLY", "SORTIE_MODIFIER_BOW_ONLY" ]; - const pushTilesetModifiers = (tileset: string): void => { - switch (tileset) { - case "GrineerForestTileset": - modifiers.push("SORTIE_MODIFIER_HAZARD_FOG"); - break; - case "CorpusShipTileset": - case "GrineerGalleonTileset": - case "InfestedCorpusShipTileset": - modifiers.push("SORTIE_MODIFIER_HAZARD_MAGNETIC"); - modifiers.push("SORTIE_MODIFIER_HAZARD_FIRE"); - modifiers.push("SORTIE_MODIFIER_HAZARD_ICE"); - break; - case "CorpusIcePlanetTileset": - case "CorpusIcePlanetTilesetCaves": - modifiers.push("SORTIE_MODIFIER_HAZARD_COLD"); - break; - } - }; if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) { - const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets]; - pushTilesetModifiers(tileset); + const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset; + pushTilesetModifiers(modifiers, tileset); const modifierType = rng.randomElement(modifiers)!; @@ -328,9 +294,12 @@ export const getSortie = (day: number): ISortie => { continue; } - const missionType = eMissionType[missionIndex].tag; + const tileset = sortieTilesets[node as keyof typeof sortieTilesets] as TSortieTileset; + pushTilesetModifiers(modifiers, tileset); - if (missionTypes.has(missionType)) { + const missionType = rng.randomElement(sortieTilesetMissions[tileset])!; + + if (missionTypes.has(missionType) || missionType == "MT_ASSASSINATION") { i--; continue; } @@ -349,9 +318,6 @@ export const getSortie = (day: number): ISortie => { modifiers.push("SORTIE_MODIFIER_SHIELDS"); } - const tileset = sortieTilesets[node as keyof typeof sortieTilesets]; - pushTilesetModifiers(tileset); - const modifierType = rng.randomElement(modifiers)!; selectedNodes.push({ diff --git a/static/fixed_responses/worldState/sortieTilesetMissions.json b/static/fixed_responses/worldState/sortieTilesetMissions.json new file mode 100644 index 00000000..36d5e194 --- /dev/null +++ b/static/fixed_responses/worldState/sortieTilesetMissions.json @@ -0,0 +1,45 @@ +{ + "CorpusGasCityTileset": ["MT_ARTIFACT", "MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"], + "CorpusIcePlanetTileset": ["MT_ASSASSINATION", "MT_DEFENSE", "MT_EXCAVATE", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_RETRIEVAL", "MT_TERRITORY"], + "CorpusIcePlanetTilesetCaves": ["MT_EXTERMINATION", "MT_INTEL"], + "CorpusOutpostTileset": [ + "MT_ARTIFACT", + "MT_ASSASSINATION", + "MT_DEFENSE", + "MT_EXCAVATE", + "MT_EXTERMINATION", + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_RESCUE", + "MT_SABOTAGE", + "MT_SURVIVAL", + "MT_TERRITORY" + ], + "CorpusShipTileset": ["MT_ARTIFACT", "MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"], + "EidolonTileset": ["MT_LANDSCAPE"], + "GrineerAsteroidTileset": ["MT_ASSASSINATION", "MT_DEFENSE", "MT_EVACUATION", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL", "MT_TERRITORY"], + "GrineerForestTileset": ["MT_ASSASSINATION", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_TERRITORY"], + "GrineerFortressTileset": ["MT_ARTIFACT", "MT_ASSAULT", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL"], + "GrineerGalleonTileset": [ + "MT_ARTIFACT", + "MT_ASSASSINATION", + "MT_DEFENSE", + "MT_EVACUATION", + "MT_EXTERMINATION", + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_RESCUE", + "MT_SABOTAGE", + "MT_SURVIVAL", + "MT_TERRITORY" + ], + "GrineerOceanTileset": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"], + "GrineerOceanTilesetAnywhere": ["MT_ASSASSINATION"], + "GrineerSettlementTileset": ["MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_TERRITORY"], + "GrineerShipyardsTileset": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_RETRIEVAL", "MT_TERRITORY"], + "InfestedCorpusShipTileset": ["MT_ASSASSINATION", "MT_HIVE", "MT_MOBILE_DEFENSE", "MT_RESCUE"], + "OrokinDerelictTileset": ["MT_ARTIFACT", "MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_SABOTAGE", "MT_SURVIVAL"], + "OrokinMoonTilesetCorpus": ["MT_ARTIFACT", "MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL"], + "OrokinMoonTilesetGrineer": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL"], + "OrokinVoidTileset": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"] +} From c56507e12dee0a7d4de8b4199e8ce552a6a88c25 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 7 May 2025 20:14:21 -0700 Subject: [PATCH 709/776] feat: friends (#2004) Closes #1288 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2004 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/addFriendController.ts | 60 ++++++++++++++++++ .../api/addIgnoredUserController.ts | 2 +- .../api/addPendingFriendController.ts | 51 ++++++++++++++++ src/controllers/api/addToGuildController.ts | 14 +++-- src/controllers/api/getFriendsController.ts | 61 +++++++++++++++---- .../api/getIgnoredUsersController.ts | 2 +- src/controllers/api/giftingController.ts | 9 ++- src/controllers/api/removeFriendController.ts | 36 +++++++++++ .../api/setFriendNoteController.ts | 30 +++++++++ .../custom/deleteAccountController.ts | 3 + src/models/friendModel.ts | 15 +++++ src/routes/api.ts | 10 ++- src/services/friendService.ts | 44 +++++++++++++ src/services/guildService.ts | 16 +---- src/types/friendTypes.ts | 24 ++++++++ src/types/guildTypes.ts | 16 +---- 16 files changed, 343 insertions(+), 50 deletions(-) create mode 100644 src/controllers/api/addFriendController.ts create mode 100644 src/controllers/api/addPendingFriendController.ts create mode 100644 src/controllers/api/removeFriendController.ts create mode 100644 src/controllers/api/setFriendNoteController.ts create mode 100644 src/models/friendModel.ts create mode 100644 src/services/friendService.ts create mode 100644 src/types/friendTypes.ts diff --git a/src/controllers/api/addFriendController.ts b/src/controllers/api/addFriendController.ts new file mode 100644 index 00000000..bbb629d9 --- /dev/null +++ b/src/controllers/api/addFriendController.ts @@ -0,0 +1,60 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { RequestHandler } from "express"; + +export const addFriendController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + const promises: Promise[] = []; + const newFriends: IFriendInfo[] = []; + if (payload.friend == "all") { + const [internalFriendships, externalFriendships] = await Promise.all([ + Friendship.find({ owner: accountId }, "friend"), + Friendship.find({ friend: accountId }, "owner") + ]); + for (const externalFriendship of externalFriendships) { + if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { + promises.push( + Friendship.insertOne({ + owner: accountId, + friend: externalFriendship.owner, + Note: externalFriendship.Note // TOVERIFY: Should the note be copied when accepting a friend request? + }) as unknown as Promise + ); + newFriends.push({ + _id: toOid(externalFriendship.owner) + }); + } + } + } else { + const externalFriendship = await Friendship.findOne({ owner: payload.friend, friend: accountId }, "Note"); + if (externalFriendship) { + promises.push( + Friendship.insertOne({ + owner: accountId, + friend: payload.friend, + Note: externalFriendship.Note + }) as unknown as Promise + ); + newFriends.push({ + _id: { $oid: payload.friend } + }); + } + } + for (const newFriend of newFriends) { + promises.push(addAccountDataToFriendInfo(newFriend)); + promises.push(addInventoryDataToFriendInfo(newFriend)); + } + await Promise.all(promises); + res.json({ + Friends: newFriends + }); +}; + +interface IAddFriendRequest { + friend: string; // oid or "all" in which case all=1 is also a query parameter +} diff --git a/src/controllers/api/addIgnoredUserController.ts b/src/controllers/api/addIgnoredUserController.ts index 99b38972..8e8d4441 100644 --- a/src/controllers/api/addIgnoredUserController.ts +++ b/src/controllers/api/addIgnoredUserController.ts @@ -2,7 +2,7 @@ import { toOid } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Account, Ignore } from "@/src/models/loginModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { IFriendInfo } from "@/src/types/guildTypes"; +import { IFriendInfo } from "@/src/types/friendTypes"; import { RequestHandler } from "express"; export const addIgnoredUserController: RequestHandler = async (req, res) => { diff --git a/src/controllers/api/addPendingFriendController.ts b/src/controllers/api/addPendingFriendController.ts new file mode 100644 index 00000000..c045d044 --- /dev/null +++ b/src/controllers/api/addPendingFriendController.ts @@ -0,0 +1,51 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { Account } from "@/src/models/loginModel"; +import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "@/src/services/friendService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { RequestHandler } from "express"; + +export const addPendingFriendController: RequestHandler = async (req, res) => { + const payload = getJSONfromString(String(req.body)); + + const account = await Account.findOne({ DisplayName: payload.friend }); + if (!account) { + res.status(400).end(); + return; + } + + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(account._id.toString(), "Settings"); + if ( + inventory.Settings?.FriendInvRestriction == "GIFT_MODE_NONE" || + (inventory.Settings?.FriendInvRestriction == "GIFT_MODE_FRIENDS" && + !(await areFriendsOfFriends(account._id, accountId))) + ) { + res.status(400).send("Friend Invite Restriction"); + return; + } + + await Friendship.insertOne({ + owner: accountId, + friend: account._id, + Note: payload.message + }); + + const friendInfo: IFriendInfo = { + _id: toOid(account._id), + DisplayName: account.DisplayName, + Note: payload.message + }; + await addInventoryDataToFriendInfo(friendInfo); + res.json({ + Friend: friendInfo + }); +}; + +interface IAddPendingFriendRequest { + friend: string; + message: string; +} diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index c67b8d1a..4ae01b7b 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -1,6 +1,7 @@ import { Guild, GuildMember } from "@/src/models/guildModel"; import { Account } from "@/src/models/loginModel"; -import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService"; +import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService"; +import { hasGuildPermission } from "@/src/services/guildService"; import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; @@ -22,15 +23,18 @@ export const addToGuildController: RequestHandler = async (req, res) => { return; } + const senderAccount = await getAccountForRequest(req); const inventory = await getInventory(account._id.toString(), "Settings"); - // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented - if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") { + if ( + inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" || + (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" && + !(await areFriends(account._id, senderAccount._id))) + ) { res.status(400).json("Invite restricted"); return; } const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; - const senderAccount = await getAccountForRequest(req); if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { res.status(400).json("Invalid permission"); } @@ -74,7 +78,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { Rank: 7, Status: 2 }; - await fillInInventoryDataForGuildMember(member); + await addInventoryDataToFriendInfo(member); res.json({ NewMember: member }); } else if ("RequestMsg" in payload) { // Player applying to join a clan diff --git a/src/controllers/api/getFriendsController.ts b/src/controllers/api/getFriendsController.ts index 1227f84d..684e9bcc 100644 --- a/src/controllers/api/getFriendsController.ts +++ b/src/controllers/api/getFriendsController.ts @@ -1,15 +1,54 @@ -import { Request, Response } from "express"; +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { Request, RequestHandler, Response } from "express"; // POST with {} instead of GET as of 38.5.0 -const getFriendsController = (_request: Request, response: Response): void => { - response.writeHead(200, { - //Connection: "keep-alive", - //"Content-Encoding": "gzip", - "Content-Type": "text/html", - // charset: "UTF - 8", - "Content-Length": "3" - }); - response.end(Buffer.from([0x7b, 0x7d, 0x0a])); +export const getFriendsController: RequestHandler = async (req: Request, res: Response) => { + const accountId = await getAccountIdForRequest(req); + const response: IGetFriendsResponse = { + Current: [], + IncomingFriendRequests: [], + OutgoingFriendRequests: [] + }; + const [internalFriendships, externalFriendships] = await Promise.all([ + Friendship.find({ owner: accountId }), + Friendship.find({ friend: accountId }, "owner Note") + ]); + for (const externalFriendship of externalFriendships) { + if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { + response.IncomingFriendRequests.push({ + _id: toOid(externalFriendship.owner), + Note: externalFriendship.Note + }); + } + } + for (const internalFriendship of internalFriendships) { + const friendInfo: IFriendInfo = { + _id: toOid(internalFriendship.friend) + }; + if (externalFriendships.find(x => x.owner.equals(internalFriendship.friend))) { + response.Current.push(friendInfo); + } else { + response.OutgoingFriendRequests.push(friendInfo); + } + } + const promises: Promise[] = []; + for (const arr of Object.values(response)) { + for (const friendInfo of arr) { + promises.push(addAccountDataToFriendInfo(friendInfo)); + promises.push(addInventoryDataToFriendInfo(friendInfo)); + } + } + await Promise.all(promises); + res.json(response); }; -export { getFriendsController }; +// interface IGetFriendsResponse { +// Current: IFriendInfo[]; +// IncomingFriendRequests: IFriendInfo[]; +// OutgoingFriendRequests: IFriendInfo[]; +// } +type IGetFriendsResponse = Record<"Current" | "IncomingFriendRequests" | "OutgoingFriendRequests", IFriendInfo[]>; diff --git a/src/controllers/api/getIgnoredUsersController.ts b/src/controllers/api/getIgnoredUsersController.ts index abadb91c..b3a6cf22 100644 --- a/src/controllers/api/getIgnoredUsersController.ts +++ b/src/controllers/api/getIgnoredUsersController.ts @@ -1,7 +1,7 @@ import { toOid } from "@/src/helpers/inventoryHelpers"; import { Account, Ignore } from "@/src/models/loginModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { IFriendInfo } from "@/src/types/guildTypes"; +import { IFriendInfo } from "@/src/types/friendTypes"; import { parallelForeach } from "@/src/utils/async-utils"; import { RequestHandler } from "express"; diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts index f965e60c..0d5c4723 100644 --- a/src/controllers/api/giftingController.ts +++ b/src/controllers/api/giftingController.ts @@ -1,5 +1,6 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Account } from "@/src/models/loginModel"; +import { areFriends } from "@/src/services/friendService"; import { createMessage } from "@/src/services/inboxService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; @@ -30,8 +31,11 @@ export const giftingController: RequestHandler = async (req, res) => { } // Cannot gift to players who have gifting disabled. - // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented - if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") { + const senderAccount = await getAccountForRequest(req); + if ( + inventory.Settings?.GiftMode == "GIFT_MODE_NONE" || + (inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id))) + ) { res.status(400).send("17").end(); return; } @@ -40,7 +44,6 @@ export const giftingController: RequestHandler = async (req, res) => { // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20) - const senderAccount = await getAccountForRequest(req); const senderInventory = await getInventory( senderAccount._id.toString(), "PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining" diff --git a/src/controllers/api/removeFriendController.ts b/src/controllers/api/removeFriendController.ts new file mode 100644 index 00000000..20243753 --- /dev/null +++ b/src/controllers/api/removeFriendController.ts @@ -0,0 +1,36 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { RequestHandler } from "express"; + +export const removeFriendGetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + if (req.query.all) { + const [internalFriendships, externalFriendships] = await Promise.all([ + Friendship.find({ owner: accountId }, "friend"), + Friendship.find({ friend: accountId }, "owner") + ]); + const promises: Promise[] = []; + const friends: IOid[] = []; + for (const externalFriendship of externalFriendships) { + if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { + promises.push(Friendship.deleteOne({ _id: externalFriendship._id }) as unknown as Promise); + friends.push(toOid(externalFriendship.owner)); + } + } + await Promise.all(promises); + res.json({ + Friends: friends + }); + } else { + const friendId = req.query.friendId as string; + await Promise.all([ + Friendship.deleteOne({ owner: accountId, friend: friendId }), + Friendship.deleteOne({ owner: friendId, friend: accountId }) + ]); + res.json({ + Friends: [{ $oid: friendId } satisfies IOid] + }); + } +}; diff --git a/src/controllers/api/setFriendNoteController.ts b/src/controllers/api/setFriendNoteController.ts new file mode 100644 index 00000000..c12543da --- /dev/null +++ b/src/controllers/api/setFriendNoteController.ts @@ -0,0 +1,30 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const setFriendNoteController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + const friendship = await Friendship.findOne({ owner: accountId, friend: payload.FriendId }, "Note Favorite"); + if (friendship) { + if ("Note" in payload) { + friendship.Note = payload.Note; + } else { + friendship.Favorite = payload.Favorite; + } + await friendship.save(); + } + res.json({ + Id: payload.FriendId, + SetNote: "Note" in payload, + Note: friendship?.Note, + Favorite: friendship?.Favorite + }); +}; + +interface ISetFriendNoteRequest { + FriendId: string; + Note?: string; + Favorite?: boolean; +} diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index 32fe4f19..449a44c4 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -10,6 +10,7 @@ import { Stats } from "@/src/models/statsModel"; import { GuildMember } from "@/src/models/guildModel"; import { Leaderboard } from "@/src/models/leaderboardModel"; import { deleteGuild } from "@/src/services/guildService"; +import { Friendship } from "@/src/models/friendModel"; export const deleteAccountController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -22,6 +23,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => { await Promise.all([ Account.deleteOne({ _id: accountId }), + Friendship.deleteMany({ owner: accountId }), + Friendship.deleteMany({ friend: accountId }), GuildMember.deleteMany({ accountId: accountId }), Ignore.deleteMany({ ignorer: accountId }), Ignore.deleteMany({ ignoree: accountId }), diff --git a/src/models/friendModel.ts b/src/models/friendModel.ts new file mode 100644 index 00000000..f253101a --- /dev/null +++ b/src/models/friendModel.ts @@ -0,0 +1,15 @@ +import { IFriendship } from "@/src/types/friendTypes"; +import { model, Schema } from "mongoose"; + +const friendshipSchema = new Schema({ + owner: { type: Schema.Types.ObjectId, required: true }, + friend: { type: Schema.Types.ObjectId, required: true }, + Note: String, + Favorite: Boolean +}); + +friendshipSchema.index({ owner: 1 }); +friendshipSchema.index({ friend: 1 }); +friendshipSchema.index({ owner: 1, friend: 1 }, { unique: true }); + +export const Friendship = model("Friendship", friendshipSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index 787164d9..8fdd94df 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -3,8 +3,10 @@ import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandon import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController"; import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; +import { addFriendController } from "@/src/controllers/api/addFriendController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController"; +import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController"; import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -101,6 +103,7 @@ import { questControlController } from "@/src/controllers/api/questControlContro import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { releasePetController } from "@/src/controllers/api/releasePetController"; +import { removeFriendGetController } from "@/src/controllers/api/removeFriendController"; import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; @@ -120,6 +123,7 @@ import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoC import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; +import { setFriendNoteController } from "@/src/controllers/api/setFriendNoteController"; import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; @@ -180,13 +184,13 @@ apiRouter.get("/getGuildContributions.php", getGuildContributionsController); apiRouter.get("/getGuildDojo.php", getGuildDojoController); apiRouter.get("/getGuildLog.php", getGuildLogController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); +apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); apiRouter.get("/getShip.php", getShipController); apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/hub", hubController); apiRouter.get("/hubInstances", hubInstancesController); apiRouter.get("/inbox.php", inboxController); -apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/inventory.php", inventoryController); apiRouter.get("/loginRewards.php", loginRewardsController); apiRouter.get("/logout.php", logoutController); @@ -196,6 +200,7 @@ apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController); apiRouter.get("/questControl.php", questControlController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); +apiRouter.get("/removeFriend.php", removeFriendGetController); apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); @@ -213,8 +218,10 @@ apiRouter.get("/updateSession.php", updateSessionGetController); // post apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); +apiRouter.post("/addFriend.php", addFriendController); apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); +apiRouter.post("/addPendingFriend.php", addPendingFriendController); apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); @@ -297,6 +304,7 @@ apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); +apiRouter.post("/setFriendNote.php", setFriendNoteController); apiRouter.post("/setGuildMotd.php", setGuildMotdController); apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); diff --git a/src/services/friendService.ts b/src/services/friendService.ts new file mode 100644 index 00000000..a17c9750 --- /dev/null +++ b/src/services/friendService.ts @@ -0,0 +1,44 @@ +import { IFriendInfo } from "../types/friendTypes"; +import { getInventory } from "./inventoryService"; +import { config } from "./configService"; +import { Account } from "../models/loginModel"; +import { Types } from "mongoose"; +import { Friendship } from "../models/friendModel"; + +export const addAccountDataToFriendInfo = async (info: IFriendInfo): Promise => { + info.DisplayName = (await Account.findById(info._id.$oid, "DisplayName"))!.DisplayName; +}; + +export const addInventoryDataToFriendInfo = async (info: IFriendInfo): Promise => { + const inventory = await getInventory(info._id.$oid, "PlayerLevel ActiveAvatarImageType"); + info.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank; + info.ActiveAvatarImageType = inventory.ActiveAvatarImageType; +}; + +export const areFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise => { + const [aAddedB, bAddedA] = await Promise.all([ + Friendship.exists({ owner: a, friend: b }), + Friendship.exists({ owner: b, friend: a }) + ]); + return Boolean(aAddedB && bAddedA); +}; + +export const areFriendsOfFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise => { + const [aInternalFriends, bInternalFriends] = await Promise.all([ + Friendship.find({ owner: a }), + Friendship.find({ owner: b }) + ]); + for (const aInternalFriend of aInternalFriends) { + if (bInternalFriends.find(x => x.friend.equals(aInternalFriend.friend))) { + const c = aInternalFriend.friend; + const [cAcceptedA, cAcceptedB] = await Promise.all([ + Friendship.exists({ owner: c, friend: a }), + Friendship.exists({ owner: c, friend: b }) + ]); + if (cAcceptedA && cAcceptedB) { + return true; + } + } + } + return false; +}; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index db6f1525..f1bb5ff9 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -24,7 +24,6 @@ import { Types } from "mongoose"; import { ExportDojoRecipes, ExportResources, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; import { logger } from "../utils/logger"; import { config } from "./configService"; -import { Account } from "../models/loginModel"; import { getRandomInt } from "./rngService"; import { Inbox } from "../models/inboxModel"; import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; @@ -32,6 +31,7 @@ import { IInventoryChanges } from "../types/purchaseTypes"; import { parallelForeach } from "../utils/async-utils"; import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json"; import { createMessage } from "./inboxService"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "./friendService"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -71,12 +71,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s if (guildMember.accountId.equals(accountId)) { missingEntry = false; } else { - dataFillInPromises.push( - (async (): Promise => { - member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; - })() - ); - dataFillInPromises.push(fillInInventoryDataForGuildMember(member)); + dataFillInPromises.push(addAccountDataToFriendInfo(member)); + dataFillInPromises.push(addInventoryDataToFriendInfo(member)); } members.push(member); } @@ -466,12 +462,6 @@ export const setDojoRoomLogFunded = (guild: TGuildDatabaseDocument, component: I } }; -export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClient): Promise => { - const inventory = await getInventory(member._id.$oid, "PlayerLevel ActiveAvatarImageType"); - member.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank; - member.ActiveAvatarImageType = inventory.ActiveAvatarImageType; -}; - export const createUniqueClanName = async (name: string): Promise => { const initialDiscriminator = getRandomInt(0, 999); let discriminator = initialDiscriminator; diff --git a/src/types/friendTypes.ts b/src/types/friendTypes.ts new file mode 100644 index 00000000..24c2f9d7 --- /dev/null +++ b/src/types/friendTypes.ts @@ -0,0 +1,24 @@ +import { Types } from "mongoose"; +import { IMongoDate, IOid } from "./commonTypes"; + +export interface IFriendInfo { + _id: IOid; + DisplayName?: string; + PlatformNames?: string[]; + PlatformAccountId?: string; + Status?: number; + ActiveAvatarImageType?: string; + LastLogin?: IMongoDate; + PlayerLevel?: number; + Suffix?: number; + Note?: string; + Favorite?: boolean; + NewRequest?: boolean; +} + +export interface IFriendship { + owner: Types.ObjectId; + friend: Types.ObjectId; + Note?: string; + Favorite?: boolean; +} diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 9dbb0d08..5f5ba8f8 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -2,6 +2,7 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "@/src/types/commonTypes"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPictureFrameInfo } from "./shipTypes"; +import { IFriendInfo } from "./friendTypes"; export interface IGuildClient { _id: IOid; @@ -104,21 +105,6 @@ export interface IGuildMemberDatabase { ShipDecorationsContributed?: ITypeCount[]; } -export interface IFriendInfo { - _id: IOid; - DisplayName?: string; - PlatformNames?: string[]; - PlatformAccountId?: string; - Status?: number; - ActiveAvatarImageType?: string; - LastLogin?: IMongoDate; - PlayerLevel?: number; - Suffix?: number; - Note?: string; - Favorite?: boolean; - NewRequest?: boolean; -} - // GuildMemberInfo export interface IGuildMemberClient extends IFriendInfo { Rank: number; From 6171b36479aa28c2e6fbd46013f6dd3bbf0b6b3c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 7 May 2025 20:14:34 -0700 Subject: [PATCH 710/776] fix: use correct day when providing pre-rollover syndicate missions (#2006) Closes #2005 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2006 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 09b1c483..cb592ba8 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1066,13 +1066,14 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. - const rng = new CRng(Date.now() >= rollover ? day : day - 1); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate"); - pushSyndicateMissions(worldState, day, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate"); + const sday = Date.now() >= rollover ? day : day - 1; + const rng = new CRng(sday); + pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate"); + pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate"); + pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate"); + pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate"); + pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate"); + pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate"); } // Archon Hunt cycling every week From 1f3bb88910a983e199add41dba3329e95ba97176 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 7 May 2025 20:15:00 -0700 Subject: [PATCH 711/776] feat: bounty bonus standing (#2009) Closes #2007 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2009 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 1344244d..cca920e0 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1096,7 +1096,7 @@ export const addMissionRewards = async ( if (syndicateEntry.Tag === "EntratiSyndicate") { const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); if (vault) currentJob = vault; - let medallionAmount = currentJob.xpAmounts[rewardInfo.JobStage]; + let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)); if ( ["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some( @@ -1119,7 +1119,11 @@ export const addMissionRewards = async ( } else { if (rewardInfo.JobTier! >= 0) { AffiliationMods.push( - addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage]) + addStanding( + inventory, + syndicateEntry.Tag, + Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)) + ) ); } else { if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) { From 0319031e1372779490071f422cc3c1193593fe59 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 7 May 2025 20:15:09 -0700 Subject: [PATCH 712/776] chore(webui): Update Russian translation (#2010) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2010 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/translations/ru.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index d02f54a5..dc4fb23d 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -62,7 +62,7 @@ dict = { login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, login_loginButton: `Войти`, - login_registerButton: `[UNTRANSLATED] Register`, + login_registerButton: `Зарегистрироваться`, navbar_logout: `Выйти`, navbar_renameAccount: `Переименовать аккаунт`, navbar_deleteAccount: `Удалить аккаунт`, @@ -120,9 +120,9 @@ dict = { mods_fingerprintHelp: `Нужна помощь с отпечатком?`, mods_rivens: `Моды Разлома`, mods_mods: `Моды`, - mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, - mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, - mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, + mods_addMissingUnrankedMods: `Добавить недостающие моды без ранга`, + mods_removeUnranked: `Удалить моды без ранга`, + mods_addMissingMaxRankMods: `Добавить недостающие моды максимального ранга`, cheats_administratorRequirement: `Вы должны быть администратором для использования этой функции. Чтобы стать администратором, добавьте \"|DISPLAYNAME|\" в administratorNames в config.json.`, cheats_server: `Сервер`, cheats_skipTutorial: `Пропустить обучение`, @@ -134,7 +134,7 @@ dict = { cheats_infiniteEndo: `Бесконечное эндо`, cheats_infiniteRegalAya: `Бесконечная Королевская Айя`, cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`, - cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, + cheats_dontSubtractConsumables: `Не уменьшать количество расходников`, cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`, cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`, cheats_unlockAllFlavourItems: `Разблокировать все уникальные предметы`, @@ -154,7 +154,7 @@ dict = { cheats_noKimCooldowns: `Чаты KIM без кулдауна`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noResourceExtractorDronesDamage: `Без урона по дронам-сборщикам`, - cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, + cheats_skipClanKeyCrafting: `Пропустить крафт кланового ключа`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, From 42c63ecbe88441db92f364a5a9431a481a645193 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 7 May 2025 20:15:19 -0700 Subject: [PATCH 713/776] chore(webui): use name for zanuka from datalist instead of webui loc (#2012) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2012 Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- static/webui/script.js | 12 +++++++++--- static/webui/translations/de.js | 3 --- static/webui/translations/en.js | 3 --- static/webui/translations/es.js | 3 --- static/webui/translations/fr.js | 3 --- static/webui/translations/ru.js | 3 --- static/webui/translations/zh.js | 3 --- 7 files changed, 9 insertions(+), 21 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index 6c18ed52..d8d30a41 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -276,13 +276,19 @@ function fetchItemList() { }, "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kDrive") }, "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": { - name: loc("code_zanukaA") + name: data.ModularParts.find( + i => i.uniqueName === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA" + ).name }, "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": { - name: loc("code_zanukaB") + name: data.ModularParts.find( + i => i.uniqueName === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB" + ).name }, "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": { - name: loc("code_zanukaC") + name: data.ModularParts.find( + i => i.uniqueName === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" + ).name } }; for (const [type, items] of Object.entries(data)) { diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 59c906c7..54f2e147 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -44,9 +44,6 @@ dict = { code_gild: `Veredeln`, code_moa: `Moa`, code_zanuka: `Jagdhund`, - code_zanukaA: `Jagdhund: Dorma`, - code_zanukaB: `Jagdhund: Bhaira`, - code_zanukaC: `Jagdhund: Hec`, code_stage: `Abschnitt`, code_complete: `Abschließen`, code_nextStage: `Nächster Abschnitt`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 4e7af430..da0d5a0d 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -43,9 +43,6 @@ dict = { code_gild: `Gild`, code_moa: `Moa`, code_zanuka: `Hound`, - code_zanukaA: `Dorma Hound`, - code_zanukaB: `Bhaira Hound`, - code_zanukaC: `Hec Hound`, code_stage: `Stage`, code_complete: `Complete`, code_nextStage: `Next stage`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 5660fafd..3caa2b83 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -44,9 +44,6 @@ dict = { code_gild: `Refinar`, code_moa: `Moa`, code_zanuka: `Sabueso`, - code_zanukaA: `Sabueso Dorma`, - code_zanukaB: `Sabueso Bhaira`, - code_zanukaC: `Sabueso Hec`, code_stage: `Etapa`, code_complete: `Completa`, code_nextStage: `Siguiente etapa`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index afaf94bf..9f35699d 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -44,9 +44,6 @@ dict = { code_gild: `Polir`, code_moa: `Moa`, code_zanuka: `Molosse`, - code_zanukaA: `Molosse Dorma`, - code_zanukaB: `Molosse Bhaira`, - code_zanukaC: `Molosse Hec`, code_stage: `Étape`, code_complete: `Compléter`, code_nextStage: `Étape suivante`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index dc4fb23d..3861d7ff 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -44,9 +44,6 @@ dict = { code_gild: `Улучшить`, code_moa: `МОА`, code_zanuka: `Гончая`, - code_zanukaA: `Гончая: Дорма`, - code_zanukaB: `Гончая: Бхайра`, - code_zanukaC: `Гончая: Хек`, code_stage: `Этап`, code_complete: `Завершить`, code_nextStage: `Cледующий этап`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 2699f0a8..dacba31e 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -44,9 +44,6 @@ dict = { code_gild: `镀金`, code_moa: `恐鸟`, code_zanuka: `猎犬`, - code_zanukaA: `铎玛猎犬`, - code_zanukaB: `拜拉猎犬`, - code_zanukaC: `骸克猎犬`, code_stage: `[UNTRANSLATED] Stage`, code_complete: `[UNTRANSLATED] Complete`, code_nextStage: `[UNTRANSLATED] Next stage`, From d831732513ab9cd97bff731cd2fbf2b9ec9f2c09 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 04:32:32 -0700 Subject: [PATCH 714/776] chore: update PE+ (#2020) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2020 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/missionInventoryUpdateService.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index beab4607..49950b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.59", + "warframe-public-export-plus": "^0.5.60", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.59", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.59.tgz", - "integrity": "sha512-/SUCVjngVDBz6gahz7CdVLywtHLODL6O5nmNtQcxFDUwrUGnF1lETcG8/UO+WLeGxBVAy4BDPbq+9ZWlYZM4uQ==" + "version": "0.5.60", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.60.tgz", + "integrity": "sha512-vMfytUc4xRi+b7RTSq+TJEl91vwEegpQKxLtXwRPfs9ZHhntxc4rmDYSNWJTvgf/aWXsFUxQlqL/GV5OLPGM7g==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index e5deb9bc..34e1c84e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.59", + "warframe-public-export-plus": "^0.5.60", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index cca920e0..5c9f6f05 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1590,9 +1590,9 @@ function getRandomMissionDrops( const drop = getRandomRewardByChance( ExportRewards[ [ - "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards", "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards", - "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards" + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards" ][Math.trunc(qualification / 3)] ][qualification % 3] ); From b987e01811e85876ee07faf3c1a485dec86090ee Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 04:51:03 -0700 Subject: [PATCH 715/776] fix: ships having wrong format in inventory response (#2018) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2018 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 | 14 +++++++------- src/models/shipModel.ts | 18 ++++++++---------- src/types/inventoryTypes/inventoryTypes.ts | 2 +- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 10d1364a..59c3415e 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -28,6 +28,7 @@ import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers"; import { version_compare } from "@/src/services/worldStateService"; import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; +import { Ship } from "@/src/models/shipModel"; export const inventoryController: RequestHandler = async (request, response) => { const account = await getAccountForRequest(request); @@ -133,13 +134,12 @@ export const getInventoryResponse = async ( xpBasedLevelCapDisabled: boolean, buildLabel: string | undefined ): Promise => { - const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>( - "LoadOutPresets" - ); - const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>( - "Ships" - ); - const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON(); + const [inventoryWithLoadOutPresets, ships] = await Promise.all([ + inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), + Ship.find({ ShipOwnerId: inventory.accountOwnerId }) + ]); + const inventoryResponse = inventoryWithLoadOutPresets.toJSON(); + inventoryResponse.Ships = ships.map(x => x.toJSON()); if (config.infiniteCredits) { inventoryResponse.RegularCredits = 999999999; diff --git a/src/models/shipModel.ts b/src/models/shipModel.ts index 5176defb..a1cd1457 100644 --- a/src/models/shipModel.ts +++ b/src/models/shipModel.ts @@ -28,17 +28,15 @@ shipSchema.set("toJSON", { delete returnedObject._id; delete returnedObject.__v; delete returnedObject.ShipOwnerId; - if (shipDatabase.ShipExteriorColors) { - shipResponse.ShipExterior = { - Colors: shipDatabase.ShipExteriorColors, - ShipAttachments: shipDatabase.ShipAttachments, - SkinFlavourItem: shipDatabase.SkinFlavourItem - }; - delete shipDatabase.ShipExteriorColors; - delete shipDatabase.ShipAttachments; - delete shipDatabase.SkinFlavourItem; - } + shipResponse.ShipExterior = { + Colors: shipDatabase.ShipExteriorColors, + ShipAttachments: shipDatabase.ShipAttachments, + SkinFlavourItem: shipDatabase.SkinFlavourItem + }; + delete shipDatabase.ShipExteriorColors; + delete shipDatabase.ShipAttachments; + delete shipDatabase.SkinFlavourItem; } }); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 23b8ec38..061f616d 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -560,7 +560,7 @@ export interface ICrewShipCustomization { export interface IShipExterior { SkinFlavourItem?: string; - Colors: IColor; + Colors?: IColor; ShipAttachments?: IShipAttachments; } From 3bcac1459b8cf92a15f48f784cd2ea37d7326bf8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 08:20:45 -0700 Subject: [PATCH 716/776] feat: track LastLogin to provide it for friends & clan members (#2013) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2013 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/addPendingFriendController.ts | 3 ++- src/controllers/api/addToGuildController.ts | 2 ++ src/controllers/api/loginController.ts | 4 +++- src/helpers/customHelpers/customHelpers.ts | 3 ++- src/models/loginModel.ts | 1 + src/services/friendService.ts | 5 ++++- src/types/loginTypes.ts | 1 + 7 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/addPendingFriendController.ts b/src/controllers/api/addPendingFriendController.ts index c045d044..0ba548f4 100644 --- a/src/controllers/api/addPendingFriendController.ts +++ b/src/controllers/api/addPendingFriendController.ts @@ -1,4 +1,4 @@ -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Friendship } from "@/src/models/friendModel"; import { Account } from "@/src/models/loginModel"; @@ -37,6 +37,7 @@ export const addPendingFriendController: RequestHandler = async (req, res) => { const friendInfo: IFriendInfo = { _id: toOid(account._id), DisplayName: account.DisplayName, + LastLogin: toMongoDate(account.LastLogin), Note: payload.message }; await addInventoryDataToFriendInfo(friendInfo); diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index 4ae01b7b..aeda4214 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -1,3 +1,4 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { Guild, GuildMember } from "@/src/models/guildModel"; import { Account } from "@/src/models/loginModel"; import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService"; @@ -75,6 +76,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { const member: IGuildMemberClient = { _id: { $oid: account._id.toString() }, DisplayName: account.DisplayName, + LastLogin: toMongoDate(account.LastLogin), Rank: 7, Status: 2 }; diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 07a9b9da..f1eab10a 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -48,7 +48,8 @@ export const loginController: RequestHandler = async (request, response) => { ConsentNeeded: false, TrackedSettings: [], Nonce: nonce, - BuildLabel: buildLabel + BuildLabel: buildLabel, + LastLogin: new Date() }); logger.debug("created new account"); response.json(createLoginResponse(myAddress, newAccount, buildLabel)); @@ -93,6 +94,7 @@ export const loginController: RequestHandler = async (request, response) => { account.Nonce = nonce; account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN"; account.BuildLabel = buildLabel; + account.LastLogin = new Date(); } await account.save(); diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts index a0163833..02defc8c 100644 --- a/src/helpers/customHelpers/customHelpers.ts +++ b/src/helpers/customHelpers/customHelpers.ts @@ -48,7 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountReq CrossPlatformAllowed: true, ForceLogoutVersion: 0, TrackedSettings: [], - Nonce: 0 + Nonce: 0, + LastLogin: new Date() } satisfies IDatabaseAccountRequiredFields; }; diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index fe9d0b5f..0f83c0cb 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -22,6 +22,7 @@ const databaseAccountSchema = new Schema( Nonce: { type: Number, default: 0 }, BuildLabel: String, Dropped: Boolean, + LastLogin: { type: Date, default: 0 }, LatestEventMessageDate: { type: Date, default: 0 }, LastLoginRewardDate: { type: Number, default: 0 }, LoginDays: { type: Number, default: 1 } diff --git a/src/services/friendService.ts b/src/services/friendService.ts index a17c9750..6243f93b 100644 --- a/src/services/friendService.ts +++ b/src/services/friendService.ts @@ -4,9 +4,12 @@ import { config } from "./configService"; import { Account } from "../models/loginModel"; import { Types } from "mongoose"; import { Friendship } from "../models/friendModel"; +import { toMongoDate } from "../helpers/inventoryHelpers"; export const addAccountDataToFriendInfo = async (info: IFriendInfo): Promise => { - info.DisplayName = (await Account.findById(info._id.$oid, "DisplayName"))!.DisplayName; + const account = (await Account.findById(info._id.$oid, "DisplayName LastLogin"))!; + info.DisplayName = account.DisplayName; + info.LastLogin = toMongoDate(account.LastLogin); }; export const addInventoryDataToFriendInfo = async (info: IFriendInfo): Promise => { diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 5a7f9e46..159d39e9 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -17,6 +17,7 @@ export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponse email: string; password: string; BuildLabel?: string; + LastLogin: Date; } export interface IDatabaseAccount extends IDatabaseAccountRequiredFields { From bdb4c8fd7c976d5d2350ea3a1b0a02b63b9c7650 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 08:20:53 -0700 Subject: [PATCH 717/776] fear: add InGameMarket to worldState (#2015) To roughly match the market as seen on live, but excluding the "premium bundles" section. Closes #1906 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2015 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../worldState/worldState.json | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 9adb9556..95b5fde2 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -22,6 +22,101 @@ "Icon": "/Lotus/Interface/Icons/DiscordIconNoBacker.png" } ], + "InGameMarket": { + "LandingPage": { + "Categories": [ + { + "CategoryName": "NEW_PLAYER", + "Name": "/Lotus/Language/Store/NewPlayerCategoryTitle", + "Icon": "newplayer", + "AddToMenu": true, + "Items": [ + "/Lotus/Types/StoreItems/Packages/2024Bundles/WeaponStarterPack", + "/Lotus/StoreItems/Powersuits/MonkeyKing/MonkeyKing", + "/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield", + "/Lotus/StoreItems/Upgrades/Skins/Effects/WerewolfEphemera", + "/Lotus/StoreItems/Types/StoreItems/SlotItems/TwoWeaponSlotItem", + "/Lotus/StoreItems/Powersuits/Wisp/Wisp", + "/Lotus/StoreItems/Weapons/Tenno/Shotgun/Shotgun", + "/Lotus/StoreItems/Weapons/Corpus/Pistols/CrpAirPistol/CrpAirPistolArray", + "/Lotus/StoreItems/Upgrades/Skins/Scarves/FlameScarf", + "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" + ] + }, + { + "CategoryName": "POPULAR", + "Name": "/Lotus/Language/Menu/StorePopular", + "Icon": "popular", + "AddToMenu": true, + "Items": [ + "/Lotus/Types/StoreItems/Packages/2025Bundles/TC2025DigitalPack", + "/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreCompSupPack", + "/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreGeminiSupPack", + "/Lotus/Types/StoreItems/Packages/WarframeBundles/TempleItemsBundle", + "/Lotus/Types/StoreItems/Packages/FormaPack", + "/Lotus/StoreItems/Upgrades/Skins/Saryn/WF1999SarynSkin", + "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriKatana/DaxDuviriKatanaWeapon", + "/Lotus/StoreItems/Upgrades/Skins/Jade/WF1999NyxSkin", + "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/NinjaColourPickerItem", + "/Lotus/StoreItems/Upgrades/Skins/Mag/WF1999MagSkin", + "/Lotus/StoreItems/Upgrades/Skins/Frost/WF1999FrostSkin", + "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriTwoHandedKatana/DaxDuviriTwoHandedKatanaWeapon", + "/Lotus/StoreItems/Upgrades/Skins/Harlequin/MirageDeluxeSkin", + "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/DaxDuviriHammer/DaxDuviriHammerWeapon" + ] + }, + { + "CategoryName": "HEIRLOOM", + "Name": "/Lotus/Language/Store/HeirloomCategoryTitle", + "Icon": "heirloom", + "AddToMenu": true, + "Items": [ + "/Lotus/Types/StoreItems/Packages/2025Bundles/RhinoHeirloomPack", + "/Lotus/Types/StoreItems/Packages/HeirloomPackRhino", + "/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoHeirloomSkin", + "/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomRhinoCrown", + "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerRhinoHeirloom", + "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardRhinoHeirloom", + "/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomRhinoGlyph", + "/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomRhinoSigil", + "/Lotus/Types/StoreItems/Packages/HeirloomPackEmber", + "/Lotus/StoreItems/Upgrades/Skins/Ember/EmberHeirloomSkin", + "/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomEmberCrown", + "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerEmberHeirloom", + "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardEmberHeirloom", + "/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomEmberGlyph", + "/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomEmberSigil" + ] + }, + { + "CategoryName": "TENNOGEN", + "Name": "/Lotus/Language/Menu/Store_Tennogen", + "Icon": "tennogen", + "AddToMenu": true, + "Items": [ + "/Lotus/StoreItems/Upgrades/Skins/Armor/SWEndocitosShoulderArmor/SWEndocitosShoulderArmorA", + "/Lotus/StoreItems/Upgrades/Skins/Scarves/SWLunariusSyandana", + "/Lotus/StoreItems/Upgrades/Skins/Scarves/SWRauSyandana", + "/Lotus/StoreItems/Upgrades/Skins/Hoplite/SWStyanaxHuzarrSkin", + "/Lotus/StoreItems/Upgrades/Skins/Werewolf/VorunaDemionnaSkin" + ] + }, + { + "CategoryName": "SALE", + "Name": "/Lotus/Language/Menu/Store_Sale", + "Icon": "sale", + "AddToMenu": true, + "Items": [] + }, + { + "CategoryName": "WISH_LIST", + "Name": "/Lotus/Language/Menu/Store_Wishlist", + "Icon": "wishlist", + "Items": [] + } + ] + } + }, "Invasions": [ { "_id": { From dab8c6c8ba565d7bdce03d0033015cbc128ba9f1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 08:21:19 -0700 Subject: [PATCH 718/776] feat: static rewards for completion of arbitration mission (#2017) Closes #1954 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2017 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 5c9f6f05..3aff3f5d 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -925,6 +925,14 @@ export const addMissionRewards = async ( }); } + if (rewardInfo.periodicMissionTag == "EliteAlert" || rewardInfo.periodicMissionTag == "EliteAlertB") { + missionCompletionCredits += 50_000; + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/Elitium", + ItemCount: 1 + }); + } + if (rewardInfo.ConquestCompleted !== undefined) { let score = 1; if (rewardInfo.ConquestHardModeActive === 1) score += 3; From 29b54b52dd41c88a37e30100cbfb1ca1316ceec7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 08:21:36 -0700 Subject: [PATCH 719/776] feat: place conclave console decoration by default for new accounts (#2019) Closes #1908 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2019 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/personalRoomsModel.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index e6176ead..9a19d06b 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -13,7 +13,7 @@ import { IPlantDatabase, IPlantClient } from "@/src/types/shipTypes"; -import { Schema, model } from "mongoose"; +import { Schema, Types, model } from "mongoose"; export const pictureFrameInfoSchema = new Schema( { @@ -153,7 +153,18 @@ const orbiterDefault: IOrbiter = { Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear Rooms: [ { Name: "AlchemyRoom", MaxCapacity: 1600 }, - { Name: "BridgeRoom", MaxCapacity: 1600 }, + { + Name: "BridgeRoom", + MaxCapacity: 1600, + PlacedDecos: [ + { + Type: "/Lotus/Objects/Tenno/Props/Ships/LandCraftPlayerProps/ConclaveConsolePlayerShipDeco", + Pos: [-30.082, -3.95954, -16.7913], + Rot: [-135, 0, 0], + _id: undefined as unknown as Types.ObjectId + } + ] + }, { Name: "LisetRoom", MaxCapacity: 1000 }, { Name: "OperatorChamberRoom", MaxCapacity: 1600 }, { Name: "OutsideRoom", MaxCapacity: 1600 }, From 3ff7e4264cc8d8b6e60df011a58c234419612965 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 08:22:06 -0700 Subject: [PATCH 720/776] chore: npm update (#2021) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2021 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 488 +++++++++++++---------------------- src/helpers/stringHelpers.ts | 2 +- src/index.ts | 6 +- 3 files changed, 181 insertions(+), 315 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49950b56..edaadd65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,9 +72,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", - "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -249,9 +249,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", - "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -296,22 +296,22 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", - "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@rxliuli/tsgo": { - "version": "2025.3.31", - "resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.3.31.tgz", - "integrity": "sha512-jEistRy/+Mu79rDv/Q8xn2yIM56WF3rfQOkwrbtivumij5HBVTfY4W3EYNL3N7rop7yg9Trew3joDohDoxQ2Ow==", + "version": "2025.5.8", + "resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.5.8.tgz", + "integrity": "sha512-P3/qxcUgiWz6nSJslJ5mMeAEqacK8LQSoOhdvHxI1/d0Xqxt2Qp6/nmhWuOlyqnCyAaIoXgoiUshiXWBGr2jaw==", "cpu": [ "x64", "ia32", @@ -382,14 +382,13 @@ } }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -427,12 +426,12 @@ } }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.15.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.16.tgz", + "integrity": "sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/qs": { @@ -504,21 +503,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", - "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", + "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/type-utils": "8.28.0", - "@typescript-eslint/utils": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/type-utils": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -534,16 +533,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", - "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", + "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "debug": "^4.3.4" }, "engines": { @@ -559,14 +558,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", - "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0" + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -577,16 +576,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", - "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", + "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/utils": "8.32.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -601,9 +600,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", - "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", "dev": true, "license": "MIT", "engines": { @@ -615,20 +614,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", - "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -642,16 +641,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -666,13 +665,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", - "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -868,16 +867,16 @@ } }, "node_modules/body-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", - "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", - "iconv-lite": "^0.5.2", + "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", @@ -887,21 +886,6 @@ "node": ">=18" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1137,9 +1121,9 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1221,16 +1205,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1406,14 +1380,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", - "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", + "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.10.2" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1564,71 +1538,47 @@ } }, "node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.0", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "cookie": "^0.7.1", "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/express/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2031,12 +1981,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -2244,9 +2194,9 @@ "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.2.tgz", - "integrity": "sha512-zbaZ+MZ2PEcAD0yINpxvlLMKzoC1GPqy5p8/ZgzRJRoB+NCczGrTX9x2ashSvkfYTitQKbV5aYQCJCiHxrzF2w==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.4.4.tgz", + "integrity": "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==", "license": "MIT" }, "node_modules/json5": { @@ -2394,15 +2344,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2418,21 +2359,21 @@ } }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "^1.53.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -2487,9 +2428,9 @@ } }, "node_modules/mongodb": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", - "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", @@ -2543,14 +2484,14 @@ } }, "node_modules/mongoose": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", - "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.14.1.tgz", + "integrity": "sha512-ijd12vjqUBr5Btqqflu0c/o8Oed5JpdaE0AKO9TjGxCgywYwnzt6ynR1ySjhgxGxrYVeXC0t1P11f1zlRiE93Q==", "license": "MIT", "dependencies": { "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.14.0", + "mongodb": "~6.16.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -2922,12 +2863,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -2981,16 +2922,18 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, "node_modules/readdirp": { @@ -3066,11 +3009,13 @@ } }, "node_modules/router": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", - "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" @@ -3152,19 +3097,18 @@ } }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", + "fresh": "^2.0.0", "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -3174,46 +3118,16 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", - "send": "^1.0.0" + "send": "^1.2.0" }, "engines": { "node": ">= 18" @@ -3383,6 +3297,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3446,20 +3369,20 @@ } }, "node_modules/synckit": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", - "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", + "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.0", + "@pkgr/core": "^0.2.3", "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/text-hex": { @@ -3498,9 +3421,9 @@ } }, "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -3706,9 +3629,9 @@ } }, "node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -3733,9 +3656,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unpipe": { @@ -3763,15 +3686,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -3808,12 +3722,12 @@ } }, "node_modules/whatwg-url": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", - "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^5.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { @@ -3890,52 +3804,6 @@ "node": ">= 12.0.0" } }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston-transport/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index 8925aea2..06d2ac28 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -2,7 +2,7 @@ import { JSONParse } from "json-with-bigint"; export const getJSONfromString = (str: string): T => { const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); - return JSONParse(jsonSubstring); + return JSONParse(jsonSubstring) as T; }; export const getSubstringFromKeyword = (str: string, keyword: string): string => { diff --git a/src/index.ts b/src/index.ts index 9a942606..f688b9e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,13 +17,11 @@ import https from "https"; import fs from "node:fs"; import { app } from "./app"; import mongoose from "mongoose"; -import { Json, JSONStringify } from "json-with-bigint"; +import { JSONStringify } from "json-with-bigint"; import { validateConfig } from "@/src/services/configWatcherService"; // Patch JSON.stringify to work flawlessly with Bigints. -JSON.stringify = (obj: Exclude, _replacer?: unknown, space?: string | number): string => { - return JSONStringify(obj, space); -}; +JSON.stringify = JSONStringify; validateConfig(); From bdd5ade2eb6a2d64b9341ebd4e2001a663f908f3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 08:22:27 -0700 Subject: [PATCH 721/776] feat: kuva siphon mission rewards (#2023) Closes #1955 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2023 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 3aff3f5d..decc74ba 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1614,6 +1614,17 @@ function getRandomMissionDrops( } } } + + if (RewardInfo.periodicMissionTag?.startsWith("KuvaMission")) { + const drop = getRandomRewardByChance( + ExportRewards[ + RewardInfo.periodicMissionTag == "KuvaMission6" || RewardInfo.periodicMissionTag == "KuvaMission12" + ? "/Lotus/Types/Game/MissionDecks/KuvaMissionRewards/KuvaSiphonFloodRewards" + : "/Lotus/Types/Game/MissionDecks/KuvaMissionRewards/KuvaSiphonRewards" + ][0] + )!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + } } return drops; } From 8d788d38a52837df6d8ffa56ac1379ef9c2e278e Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 17:37:00 -0700 Subject: [PATCH 722/776] chore: remove query for ship in getShipController (#2022) as far as I can tell, the ShipAttachments and SkinFlavourItem are just here due to the fact that the type from ShipExterior is being reused, but they aren't actually needed because the interior can't have attachments or flavour items - and if it could, they would be different from the exterior anyway. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2022 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/getShipController.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/getShipController.ts b/src/controllers/api/getShipController.ts index c68ff789..ca22ec3d 100644 --- a/src/controllers/api/getShipController.ts +++ b/src/controllers/api/getShipController.ts @@ -3,7 +3,6 @@ import { config } from "@/src/services/configService"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; -import { getShip } from "@/src/services/shipService"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { IGetShipResponse } from "@/src/types/shipTypes"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; @@ -21,7 +20,6 @@ export const getShipController: RequestHandler = async (req, res) => { const personalRooms = personalRoomsDb.toJSON(); const loadout = await getLoadout(accountId); - const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem"); const getShipResponse: IGetShipResponse = { ShipOwnerId: accountId, @@ -31,8 +29,8 @@ export const getShipController: RequestHandler = async (req, res) => { ShipId: toOid(personalRoomsDb.activeShipId), ShipInterior: { Colors: personalRooms.ShipInteriorColors, - ShipAttachments: ship.ShipAttachments, - SkinFlavourItem: ship.SkinFlavourItem + ShipAttachments: { HOOD_ORNAMENT: "" }, + SkinFlavourItem: "" }, FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId ? toOid(personalRooms.Ship.FavouriteLoadoutId) From 0e255067a860ccd9fd0b3deb1f3ecbabfaf0c3e6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 8 May 2025 18:00:22 -0700 Subject: [PATCH 723/776] chore: increase seed ranges to be more accurate (#2029) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2029 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index cb592ba8..a9a65efa 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -5,7 +5,7 @@ import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMiss import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; -import { CRng } from "@/src/services/rngService"; +import { CRng, SRng } from "@/src/services/rngService"; import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, @@ -234,7 +234,7 @@ const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): voi }; export const getSortie = (day: number): ISortie => { - const seed = new CRng(day).randomInt(0, 0xffff); + const seed = new CRng(day).randomInt(0, 100_000); const rng = new CRng(seed); const boss = rng.randomElement(sortieBosses)!; @@ -350,7 +350,7 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { const dayStart = EPOCH + day * 86400000; const dayEnd = EPOCH + (day + 3) * 86400000; - const rng = new CRng(new CRng(day).randomInt(0, 0xffff)); + const rng = new CRng(new CRng(day).randomInt(0, 100_000)); return { _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, Daily: true, @@ -370,7 +370,7 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; - const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff)); + const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000)); return { _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, @@ -387,7 +387,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; - const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff)); + const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000)); return { _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, @@ -431,7 +431,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], // TODO: xpAmounts need to be calculated based on the jobType somehow? - const seed = new CRng(bountyCycle).randomInt(0, 0xffff); + const seed = new CRng(bountyCycle).randomInt(0, 100_000); const bountyCycleStart = bountyCycle * 9000000; const bountyCycleEnd = bountyCycleStart + 9000000; @@ -700,7 +700,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => { //logger.debug(`birthday on day ${day}`); eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0 } - const rng = new CRng(new CRng(week).randomInt(0, 0xffff)); + const rng = new CRng(new CRng(week).randomInt(0, 100_000)); const challenges = [ "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy", "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium", @@ -981,7 +981,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Elite Sanctuary Onslaught cycling every week - worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new CRng(week).randomInt(0, 0xffff); + worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(BigInt(week)).randomInt(0, 0xff_ffff); // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation let bountyCycle = Math.trunc(Date.now() / 9000000); @@ -1066,14 +1066,14 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. - const sday = Date.now() >= rollover ? day : day - 1; - const rng = new CRng(sday); - pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate"); - pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate"); - pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate"); - pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate"); - pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate"); - pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate"); + const sdy = Date.now() >= rollover ? day : day - 1; + const rng = new CRng(sdy); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48050", "PerrinSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4805e", "RedVeilSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48061", "SteelMeridianSyndicate"); } // Archon Hunt cycling every week @@ -1183,7 +1183,7 @@ export const getLiteSortie = (week: number): ILiteSortie => { } } - const seed = new CRng(week).randomInt(0, 0xffff); + const seed = new CRng(week).randomInt(0, 100_000); const rng = new CRng(seed); const firstNodeIndex = rng.randomInt(0, nodes.length - 1); const firstNode = nodes[firstNodeIndex]; From 7a51fab5d3d6c2ed8e7960e0f916840280875171 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 07:18:25 +0200 Subject: [PATCH 724/776] chore: address some questionable calls to DocumentArray.id --- src/controllers/api/guildTechController.ts | 2 +- src/controllers/api/setDojoComponentSettingsController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 821831d4..49d96b70 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -104,7 +104,7 @@ export const guildTechController: RequestHandler = async (req, res) => { ) { throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); } - if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) { + if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) { throw new Error( `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array` ); diff --git a/src/controllers/api/setDojoComponentSettingsController.ts b/src/controllers/api/setDojoComponentSettingsController.ts index e7118415..1286ba25 100644 --- a/src/controllers/api/setDojoComponentSettingsController.ts +++ b/src/controllers/api/setDojoComponentSettingsController.ts @@ -13,7 +13,7 @@ export const setDojoComponentSettingsController: RequestHandler = async (req, re res.json({ DojoRequestStatus: -1 }); return; } - const component = guild.DojoComponents.id(req.query.componentId)!; + const component = guild.DojoComponents.id(req.query.componentId as string)!; const data = getJSONfromString(String(req.body)); component.Settings = data.Settings; await guild.save(); From f9b3fecc10f790977d1da23aad642a10d8316b16 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 00:20:54 -0700 Subject: [PATCH 725/776] chore: some initial handling of legacy oid format (#2033) This at least allows mission inventory update to succeed on U19.5 and below. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2033 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/claimCompletedRecipeController.ts | 16 ++++---- src/controllers/api/inventoryController.ts | 41 +++++++++++++------ src/controllers/api/loginController.ts | 2 +- src/controllers/api/setGuildMotdController.ts | 2 +- src/controllers/custom/addXpController.ts | 3 +- src/helpers/inventoryHelpers.ts | 41 ++++++++++++++++++- src/helpers/nemesisHelpers.ts | 3 +- src/services/inventoryService.ts | 7 ++-- src/services/missionInventoryUpdateService.ts | 3 +- src/services/worldStateService.ts | 18 +------- src/types/commonTypes.ts | 5 +++ .../inventoryTypes/commonInventoryTypes.ts | 4 +- src/types/inventoryTypes/inventoryTypes.ts | 4 +- 13 files changed, 96 insertions(+), 53 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index f1fa6f6e..173c7221 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -4,9 +4,9 @@ import { RequestHandler } from "express"; import { logger } from "@/src/utils/logger"; import { getRecipe } from "@/src/services/itemDataService"; -import { IOid } from "@/src/types/commonTypes"; +import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { getInventory, updateCurrency, @@ -18,7 +18,7 @@ import { import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { toOid2 } from "@/src/helpers/inventoryHelpers"; interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -26,10 +26,8 @@ interface IClaimCompletedRecipeRequest { export const claimCompletedRecipeController: RequestHandler = async (req, res) => { const claimCompletedRecipeRequest = getJSONfromString(String(req.body)); - const accountId = await getAccountIdForRequest(req); - if (!accountId) throw new Error("no account id"); - - const inventory = await getInventory(accountId); + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString()); const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid); if (!pendingRecipe) { throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); @@ -81,7 +79,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } else { logger.debug("Claiming Recipe", { recipe, pendingRecipe }); - let BrandedSuits: undefined | IOid[]; + let BrandedSuits: undefined | IOidWithLegacySupport[]; if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { inventory.PendingSpectreLoadouts ??= []; inventory.SpectreLoadouts ??= []; @@ -106,7 +104,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), 1 ); - BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)]; + BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)]; } let InventoryChanges: IInventoryChanges = {}; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 59c3415e..a26a73c5 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -25,10 +25,10 @@ import { logger } from "@/src/utils/logger"; import { catBreadHash } from "@/src/helpers/stringHelpers"; import { Types } from "mongoose"; import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers"; -import { version_compare } from "@/src/services/worldStateService"; import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { Ship } from "@/src/models/shipModel"; +import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers"; export const inventoryController: RequestHandler = async (request, response) => { const account = await getAccountForRequest(request); @@ -306,19 +306,34 @@ export const getInventoryResponse = async ( // Set 2FA enabled so trading post can be used inventoryResponse.HWIDProtectEnabled = true; - // Fix nemesis for older versions - if ( - inventoryResponse.Nemesis && - buildLabel && - !isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel) - ) { - inventoryResponse.Nemesis = undefined; - } + if (buildLabel) { + // Fix nemesis for older versions + if (inventoryResponse.Nemesis && !isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel)) { + inventoryResponse.Nemesis = undefined; + } - if (buildLabel && version_compare(buildLabel, "2018.02.22.14.34") < 0) { - const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString()); - const personalRooms = personalRoomsDb.toJSON(); - inventoryResponse.Ship = personalRooms.Ship; + if (version_compare(buildLabel, "2018.02.22.14.34") < 0) { + const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString()); + const personalRooms = personalRoomsDb.toJSON(); + inventoryResponse.Ship = personalRooms.Ship; + + if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) { + // U19.5 and below use $id instead of $oid + for (const category of equipmentKeys) { + for (const item of inventoryResponse[category]) { + toLegacyOid(item.ItemId); + } + } + for (const upgrade of inventoryResponse.Upgrades) { + toLegacyOid(upgrade.ItemId); + } + if (inventoryResponse.BrandedSuits) { + for (const id of inventoryResponse.BrandedSuits) { + toLegacyOid(id); + } + } + } + } } return inventoryResponse; diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index f1eab10a..2007db12 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -7,7 +7,7 @@ import { Account } from "@/src/models/loginModel"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; import { logger } from "@/src/utils/logger"; -import { version_compare } from "@/src/services/worldStateService"; +import { version_compare } from "@/src/helpers/inventoryHelpers"; 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 diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts index 62f10521..1e09ab28 100644 --- a/src/controllers/api/setGuildMotdController.ts +++ b/src/controllers/api/setGuildMotdController.ts @@ -1,8 +1,8 @@ +import { version_compare } from "@/src/helpers/inventoryHelpers"; import { Alliance, Guild, GuildMember } from "@/src/models/guildModel"; import { hasGuildPermissionEx } from "@/src/services/guildService"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; -import { version_compare } from "@/src/services/worldStateService"; import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; diff --git a/src/controllers/custom/addXpController.ts b/src/controllers/custom/addXpController.ts index 0ca05102..7e42deb3 100644 --- a/src/controllers/custom/addXpController.ts +++ b/src/controllers/custom/addXpController.ts @@ -1,5 +1,6 @@ import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; @@ -11,7 +12,7 @@ export const addXpController: RequestHandler = async (req, res) => { const request = req.body as IAddXpRequest; for (const [category, gear] of Object.entries(request)) { for (const clientItem of gear) { - const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid); + const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid); if (dbItem) { if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) { if ((dbItem.Polarized ?? 0) < 5) { diff --git a/src/helpers/inventoryHelpers.ts b/src/helpers/inventoryHelpers.ts index 5efc801d..6ced29e9 100644 --- a/src/helpers/inventoryHelpers.ts +++ b/src/helpers/inventoryHelpers.ts @@ -1,9 +1,46 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { Types } from "mongoose"; import { TRarity } from "warframe-public-export-plus"; +export const version_compare = (a: string, b: string): number => { + const a_digits = a + .split("/")[0] + .split(".") + .map(x => parseInt(x)); + const b_digits = b + .split("/")[0] + .split(".") + .map(x => parseInt(x)); + for (let i = 0; i != a_digits.length; ++i) { + if (a_digits[i] != b_digits[i]) { + return a_digits[i] > b_digits[i] ? 1 : -1; + } + } + return 0; +}; + export const toOid = (objectId: Types.ObjectId): IOid => { - return { $oid: objectId.toString() } satisfies IOid; + return { $oid: objectId.toString() }; +}; + +export function toOid2(objectId: Types.ObjectId, buildLabel: undefined): IOid; +export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport; +export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport { + if (buildLabel && version_compare(buildLabel, "2016.12.21.19.13") <= 0) { + return { $id: objectId.toString() }; + } + return { $oid: objectId.toString() }; +} + +export const toLegacyOid = (oid: IOidWithLegacySupport): void => { + if (!("$id" in oid)) { + oid.$id = oid.$oid; + delete oid.$oid; + } +}; + +export const fromOid = (oid: IOidWithLegacySupport): string => { + return (oid.$oid ?? oid.$id)!; }; export const toMongoDate = (date: Date): IMongoDate => { diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 5df1ad9a..6e9db804 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -6,9 +6,10 @@ import { logger } from "../utils/logger"; import { IOid } from "../types/commonTypes"; import { Types } from "mongoose"; import { addMods, generateRewardSeed } from "../services/inventoryService"; -import { isArchwingMission, version_compare } from "../services/worldStateService"; +import { isArchwingMission } from "../services/worldStateService"; import { fromStoreItem, toStoreItem } from "../services/itemDataService"; import { createMessage } from "../services/inboxService"; +import { version_compare } from "./inventoryHelpers"; export const getInfNodes = (faction: string, rank: number): IInfNode[] => { const infNodes = []; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 6a30df38..2beba2e7 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -70,6 +70,7 @@ import { createShip } from "./shipService"; import { catbrowDetails, fromMongoDate, + fromOid, kubrowDetails, kubrowFurPatternsWeights, kubrowWeights, @@ -423,7 +424,7 @@ export const addItem = async ( changes.push({ ItemType: egg.ItemType, ExpirationDate: { $date: { $numberLong: "2000000000000" } }, - ItemId: toOid(egg._id) + ItemId: toOid(egg._id) // TODO: Pass on buildLabel from purchaseService }); } return { @@ -1491,9 +1492,9 @@ export const applyClientEquipmentUpdates = ( const category = inventory[categoryName]; gearArray.forEach(({ ItemId, XP, InfestationDate }) => { - const item = category.id(ItemId.$oid); + const item = category.id(fromOid(ItemId)); if (!item) { - throw new Error(`No item with id ${ItemId.$oid} in ${categoryName}`); + throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`); } if (XP) { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index decc74ba..a585dceb 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -62,6 +62,7 @@ import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClass import { config } from "./configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { ISyndicateMissionInfo } from "../types/worldStateTypes"; +import { fromOid } from "../helpers/inventoryHelpers"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C @@ -399,7 +400,7 @@ export const addMissionInventoryUpdates = async ( break; case "Upgrades": value.forEach(clientUpgrade => { - const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!; + const upgrade = inventory.Upgrades.id(fromOid(clientUpgrade.ItemId))!; upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress }); break; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index a9a65efa..6b953518 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -18,6 +18,7 @@ import { ISyndicateMissionInfo, IWorldState } from "../types/worldStateTypes"; +import { version_compare } from "../helpers/inventoryHelpers"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -1239,20 +1240,3 @@ export const isArchwingMission = (node: IRegion): boolean => { } return false; }; - -export const version_compare = (a: string, b: string): number => { - const a_digits = a - .split("/")[0] - .split(".") - .map(x => parseInt(x)); - const b_digits = b - .split("/")[0] - .split(".") - .map(x => parseInt(x)); - for (let i = 0; i != a_digits.length; ++i) { - if (a_digits[i] != b_digits[i]) { - return a_digits[i] > b_digits[i] ? 1 : -1; - } - } - return 0; -}; diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts index 5ac1cac3..a9335fff 100644 --- a/src/types/commonTypes.ts +++ b/src/types/commonTypes.ts @@ -4,6 +4,11 @@ export interface IOid { $oid: string; } +export interface IOidWithLegacySupport { + $oid?: string; + $id?: string; +} + export interface IMongoDate { $date: { $numberLong: string; diff --git a/src/types/inventoryTypes/commonInventoryTypes.ts b/src/types/inventoryTypes/commonInventoryTypes.ts index 37168a7d..8cc0d56f 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -1,4 +1,4 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { Types } from "mongoose"; import { ICrewShipCustomization, @@ -92,7 +92,7 @@ export interface IEquipmentClient IEquipmentDatabase, "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details" > { - ItemId: IOid; + ItemId: IOidWithLegacySupport; InfestationDate?: IMongoDate; Expiry?: IMongoDate; UpgradesExpiry?: IMongoDate; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 061f616d..a32fc796 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Types } from "mongoose"; -import { IOid, IMongoDate } from "../commonTypes"; +import { IOid, IMongoDate, IOidWithLegacySupport } from "../commonTypes"; import { IColor, IItemConfig, @@ -371,7 +371,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EchoesHexConquestCacheScoreMission?: number; EchoesHexConquestActiveFrameVariants?: string[]; EchoesHexConquestActiveStickers?: string[]; - BrandedSuits?: IOid[]; + BrandedSuits?: IOidWithLegacySupport[]; LockedWeaponGroup?: ILockedWeaponGroupClient; HubNpcCustomizations?: IHubNpcCustomization[]; Ship?: IOrbiter; // U22 and below, response only From 1084932afbd6d1f27fe8e3b8a6b63b6274f453e8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:35:58 -0700 Subject: [PATCH 726/776] fix: only set IsNew flag if the ItemType is new (#2028) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2028 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2beba2e7..dd3b229b 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -870,10 +870,14 @@ const addSentinel = ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades); - const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined; const sentinelIndex = - inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features, IsNew: true }) - - 1; + inventory.Sentinels.push({ + ItemType: sentinelName, + Configs: configs, + XP: 0, + Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined, + IsNew: inventory.Sentinels.find(x => x.ItemType == sentinelName) ? undefined : true + }) - 1; inventoryChanges.Sentinels ??= []; inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON()); @@ -922,6 +926,9 @@ export const addPowerSuit = async ( }, defaultOverwrites ); + if (suit.IsNew) { + suit.IsNew = !inventory.Suits.find(x => x.ItemType == powersuitName); + } if (!suit.IsNew) { suit.IsNew = undefined; } @@ -956,7 +963,7 @@ export const addMechSuit = async ( UpgradeVer: 101, XP: 0, Features: features, - IsNew: true + IsNew: inventory.MechSuits.find(x => x.ItemType == mechsuitName) ? undefined : true }) - 1; inventoryChanges.MechSuits ??= []; inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); @@ -996,7 +1003,7 @@ export const addSpaceSuit = ( UpgradeVer: 101, XP: 0, Features: features, - IsNew: true + IsNew: inventory.SpaceSuits.find(x => x.ItemType == spacesuitName) ? undefined : true }) - 1; inventoryChanges.SpaceSuits ??= []; inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON()); @@ -1078,7 +1085,7 @@ export const addKubrowPet = ( Configs: configs, XP: 0, Details: details, - IsNew: true + IsNew: inventory.KubrowPets.find(x => x.ItemType == kubrowPetName) ? undefined : true }) - 1; inventoryChanges.KubrowPets ??= []; inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON()); @@ -1258,6 +1265,9 @@ export const addEquipment = ( }, defaultOverwrites ); + if (equipment.IsNew) { + equipment.IsNew = !inventory[category].find(x => x.ItemType == type); + } if (!equipment.IsNew) { equipment.IsNew = undefined; } From 3fc2dccf81da82c1e44c07c469c798518afcca31 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:36:22 -0700 Subject: [PATCH 727/776] chore: use 64-bit RNG everywhere (#2030) Closes #2026 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2030 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/modularWeaponSaleController.ts | 4 +- src/services/loginRewardService.ts | 12 ++--- src/services/rngService.ts | 48 ++----------------- src/services/serversideVendorsService.ts | 11 ++--- src/services/worldStateService.ts | 32 ++++++------- 5 files changed, 32 insertions(+), 75 deletions(-) diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index 3e37547b..767d1a94 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -2,7 +2,7 @@ import { RequestHandler } from "express"; import { ExportWeapons } from "warframe-public-export-plus"; import { IMongoDate } from "@/src/types/commonTypes"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; -import { CRng } from "@/src/services/rngService"; +import { SRng } from "@/src/services/rngService"; import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { @@ -140,7 +140,7 @@ const getModularWeaponSale = ( partTypes: string[], getItemType: (parts: string[]) => string ): IModularWeaponSaleInfo => { - const rng = new CRng(day); + const rng = new SRng(day); const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!); let partsCost = 0; for (const part of parts) { diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index 1734247a..1f94807b 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -1,7 +1,7 @@ import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json"; import { IInventoryChanges } from "../types/purchaseTypes"; import { TAccountDocument } from "./loginService"; -import { CRng, mixSeeds } from "./rngService"; +import { mixSeeds, SRng } from "./rngService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { addBooster, updateCurrency } from "./inventoryService"; import { handleStoreItemAcquisition } from "./purchaseService"; @@ -49,8 +49,8 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu // Always produces the same result for the same account _id & LoginDays pair. export const isLoginRewardAChoice = (account: TAccountDocument): boolean => { const accountSeed = parseInt(account._id.toString().substring(16), 16); - const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); - return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed. + const rng = new SRng(mixSeeds(accountSeed, account.LoginDays)); + return rng.randomFloat() < 0.25; }; // Always produces the same result for the same account _id & LoginDays pair. @@ -59,8 +59,8 @@ export const getRandomLoginRewards = ( inventory: TInventoryDatabaseDocument ): ILoginReward[] => { const accountSeed = parseInt(account._id.toString().substring(16), 16); - const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); - const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed. + const rng = new SRng(mixSeeds(accountSeed, account.LoginDays)); + const pick_a_door = rng.randomFloat() < 0.25; const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)]; if (pick_a_door) { do { @@ -73,7 +73,7 @@ export const getRandomLoginRewards = ( return rewards; }; -const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { +const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { const reward = rng.randomReward(randomRewards)!; //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; if (reward.RewardType == "RT_RANDOM_RECIPE") { diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 0e836466..597ae01b 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -86,54 +86,12 @@ export const mixSeeds = (seed1: number, seed2: number): number => { return seed >>> 0; }; -// Seeded RNG for internal usage. Based on recommendations in the ISO C standards. -export class CRng { - state: number; - - constructor(seed: number = 1) { - this.state = seed; - } - - random(): number { - this.state = (this.state * 1103515245 + 12345) & 0x7fffffff; - return (this.state & 0x3fffffff) / 0x3fffffff; - } - - randomInt(min: number, max: number): number { - const diff = max - min; - if (diff != 0) { - if (diff < 0) { - throw new Error(`max must be greater than min`); - } - if (diff > 0x3fffffff) { - throw new Error(`insufficient entropy`); - } - min += Math.floor(this.random() * (diff + 1)); - } - return min; - } - - randomElement(arr: readonly T[]): T | undefined { - return arr[Math.floor(this.random() * arr.length)]; - } - - randomReward(pool: T[]): T | undefined { - return getRewardAtPercentage(pool, this.random()); - } - - churnSeed(its: number): void { - while (its--) { - this.state = (this.state * 1103515245 + 12345) & 0x7fffffff; - } - } -} - -// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth. +// Seeded RNG with identical results to the game client. Based on work by Donald Knuth. export class SRng { state: bigint; - constructor(seed: bigint) { - this.state = seed; + constructor(seed: bigint | number) { + this.state = BigInt(seed); } randomInt(min: number, max: number): number { diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 171c60fc..af7d0b91 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,6 +1,6 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { catBreadHash } from "@/src/helpers/stringHelpers"; -import { CRng, mixSeeds } from "@/src/services/rngService"; +import { mixSeeds, SRng } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { ExportVendors, IRange } from "warframe-public-export-plus"; @@ -204,7 +204,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000; const cycleDuration = vendorInfo.cycleDuration; const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration); - const rng = new CRng(mixSeeds(vendorSeed, cycleIndex)); + const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); const manifest = ExportVendors[vendorInfo.TypeName]; const offersToAdd = []; if (manifest.numItems && !manifest.isOneBinPerCycle) { @@ -247,8 +247,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani $oid: ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + vendorInfo._id.$oid.substring(8, 16) + - rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") + - rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") + rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0") } }; if (rawItem.numRandomItemPrices) { @@ -283,9 +282,9 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani item.PremiumPrice = [value, value]; } if (vendorInfo.RandomSeedType) { - item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); + item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff); if (vendorInfo.RandomSeedType == "VRST_WEAPON") { - const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff); + const highDword = rng.randomInt(0, 0xffff_ffff); item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); } } diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 6b953518..3d4821a1 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -5,7 +5,7 @@ import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMiss import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; -import { CRng, SRng } from "@/src/services/rngService"; +import { SRng } from "@/src/services/rngService"; import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, @@ -193,7 +193,7 @@ const pushSyndicateMissions = ( ): void => { const nodeOptions: string[] = [...syndicateMissions]; - const rng = new CRng(seed); + const rng = new SRng(seed); const nodes: string[] = []; for (let i = 0; i != 6; ++i) { const index = rng.randomInt(0, nodeOptions.length - 1); @@ -235,8 +235,8 @@ const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): voi }; export const getSortie = (day: number): ISortie => { - const seed = new CRng(day).randomInt(0, 100_000); - const rng = new CRng(seed); + const seed = new SRng(day).randomInt(0, 100_000); + const rng = new SRng(seed); const boss = rng.randomElement(sortieBosses)!; @@ -351,7 +351,7 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { const dayStart = EPOCH + day * 86400000; const dayEnd = EPOCH + (day + 3) * 86400000; - const rng = new CRng(new CRng(day).randomInt(0, 100_000)); + const rng = new SRng(new SRng(day).randomInt(0, 100_000)); return { _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, Daily: true, @@ -371,7 +371,7 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; - const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000)); + const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000)); return { _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, @@ -388,7 +388,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; - const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000)); + const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000)); return { _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, @@ -432,12 +432,12 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], // TODO: xpAmounts need to be calculated based on the jobType somehow? - const seed = new CRng(bountyCycle).randomInt(0, 100_000); + const seed = new SRng(bountyCycle).randomInt(0, 100_000); const bountyCycleStart = bountyCycle * 9000000; const bountyCycleEnd = bountyCycleStart + 9000000; { - const rng = new CRng(seed); + const rng = new SRng(seed); syndicateMissions.push({ _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" @@ -509,7 +509,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], } { - const rng = new CRng(seed); + const rng = new SRng(seed); syndicateMissions.push({ _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025" @@ -581,7 +581,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], } { - const rng = new CRng(seed); + const rng = new SRng(seed); syndicateMissions.push({ _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002" @@ -701,7 +701,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => { //logger.debug(`birthday on day ${day}`); eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0 } - const rng = new CRng(new CRng(week).randomInt(0, 100_000)); + const rng = new SRng(new SRng(week).randomInt(0, 100_000)); const challenges = [ "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy", "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium", @@ -982,7 +982,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Elite Sanctuary Onslaught cycling every week - worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(BigInt(week)).randomInt(0, 0xff_ffff); + worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff); // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation let bountyCycle = Math.trunc(Date.now() / 9000000); @@ -1068,7 +1068,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. const sdy = Date.now() >= rollover ? day : day - 1; - const rng = new CRng(sdy); + const rng = new SRng(sdy); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate"); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate"); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate"); @@ -1184,8 +1184,8 @@ export const getLiteSortie = (week: number): ILiteSortie => { } } - const seed = new CRng(week).randomInt(0, 100_000); - const rng = new CRng(seed); + const seed = new SRng(week).randomInt(0, 100_000); + const rng = new SRng(seed); const firstNodeIndex = rng.randomInt(0, nodes.length - 1); const firstNode = nodes[firstNodeIndex]; nodes.splice(firstNodeIndex, 1); From ab32728c47d2f22d956b18e29db3a5a371a9cab2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:36:49 -0700 Subject: [PATCH 728/776] fix: don't give assassination blueprint reward for archon hunt (#2031) Closes #2025 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2031 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a585dceb..a7172bf6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1372,20 +1372,16 @@ function getRandomMissionDrops( // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"]; } else if (RewardInfo.sortieId) { - // Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. Assassinations are an exception to this. + // Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. + // Assassinations in non-lite sorties are an exception to this. if (region.missionIndex == 0) { const arr = RewardInfo.sortieId.split("_"); - let sortieId = arr[1]; - if (sortieId == "Lite") { - sortieId = arr[2]; - } - const sortie = getSortie(idToDay(sortieId)); - const mission = sortie.Variants.find(x => x.node == arr[0])!; - if (mission.missionType == "MT_ASSASSINATION") { - rewardManifests = region.rewardManifests; - } else { - rewardManifests = []; + let giveNodeReward = false; + if (arr[1] != "Lite") { + const sortie = getSortie(idToDay(arr[1])); + giveNodeReward = sortie.Variants.find(x => x.node == arr[0])!.missionType == "MT_ASSASSINATION"; } + rewardManifests = giveNodeReward ? region.rewardManifests : []; } else { rewardManifests = []; } From 31043b55de0b533f2ded3a3760744c81fcd82e3f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:37:09 -0700 Subject: [PATCH 729/776] feat: batch remove friends (#2032) Closes #1947 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2032 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/removeFriendController.ts | 69 ++++++++++++++++++- src/routes/api.ts | 3 +- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/removeFriendController.ts b/src/controllers/api/removeFriendController.ts index 20243753..24b39c2e 100644 --- a/src/controllers/api/removeFriendController.ts +++ b/src/controllers/api/removeFriendController.ts @@ -1,8 +1,13 @@ import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Friendship } from "@/src/models/friendModel"; +import { Account } from "@/src/models/loginModel"; +import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IOid } from "@/src/types/commonTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; import { RequestHandler } from "express"; +import { Types } from "mongoose"; export const removeFriendGetController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -22,7 +27,7 @@ export const removeFriendGetController: RequestHandler = async (req, res) => { await Promise.all(promises); res.json({ Friends: friends - }); + } satisfies IRemoveFriendsResponse); } else { const friendId = req.query.friendId as string; await Promise.all([ @@ -30,7 +35,65 @@ export const removeFriendGetController: RequestHandler = async (req, res) => { Friendship.deleteOne({ owner: friendId, friend: accountId }) ]); res.json({ - Friends: [{ $oid: friendId } satisfies IOid] - }); + Friends: [{ $oid: friendId }] + } satisfies IRemoveFriendsResponse); } }; + +export const removeFriendPostController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend)); + // TOVERIFY: Should pending friendships also be kept? + + // Keep friends that have been online within threshold + await parallelForeach([...friends], async friend => { + const account = (await Account.findById(friend, "LastLogin"))!; + const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000; + if (daysLoggedOut < data.DaysLoggedOut) { + friends.delete(friend); + } + }); + + if (data.SkipClanmates) { + const inventory = await getInventory(accountId, "GuildId"); + if (inventory.GuildId) { + await parallelForeach([...friends], async friend => { + const friendInventory = await getInventory(friend.toString(), "GuildId"); + if (friendInventory.GuildId?.equals(inventory.GuildId)) { + friends.delete(friend); + } + }); + } + } + + // Remove all remaining friends that aren't in SkipFriendIds & give response. + const promises = []; + const response: IOid[] = []; + for (const friend of friends) { + if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) { + promises.push(Friendship.deleteOne({ owner: accountId, friend: friend })); + promises.push(Friendship.deleteOne({ owner: friend, friend: accountId })); + response.push(toOid(friend)); + } + } + await Promise.all(promises); + res.json({ + Friends: response + } satisfies IRemoveFriendsResponse); +}; + +// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075. +const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => { + return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8); +}; + +interface IBatchRemoveFriendsRequest { + DaysLoggedOut: number; + SkipClanmates: boolean; + SkipFriendIds: string[]; +} + +interface IRemoveFriendsResponse { + Friends: IOid[]; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 8fdd94df..1ce340b8 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -103,7 +103,7 @@ import { questControlController } from "@/src/controllers/api/questControlContro import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { releasePetController } from "@/src/controllers/api/releasePetController"; -import { removeFriendGetController } from "@/src/controllers/api/removeFriendController"; +import { removeFriendGetController, removeFriendPostController } from "@/src/controllers/api/removeFriendController"; import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; @@ -290,6 +290,7 @@ apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/questControl.php", questControlController); // U17 apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/releasePet.php", releasePetController); +apiRouter.post("/removeFriend.php", removeFriendPostController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); From 3d13ec311eb72acc01acdc62a53a491af2a28005 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:37:28 -0700 Subject: [PATCH 730/776] feat: claimingBlueprintRefundsIngredients cheat (#2034) Closes #1922 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2034 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- config.json.example | 1 + .../api/claimCompletedRecipeController.ts | 71 +++++++++++-------- src/services/configService.ts | 1 + src/services/inventoryService.ts | 25 ++++--- static/webui/index.html | 4 ++ 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/zh.js | 1 + 11 files changed, 69 insertions(+), 39 deletions(-) diff --git a/config.json.example b/config.json.example index e8c32ed1..c464ad1a 100644 --- a/config.json.example +++ b/config.json.example @@ -19,6 +19,7 @@ "infiniteEndo": false, "infiniteRegalAya": false, "infiniteHelminthMaterials": false, + "claimingBlueprintRefundsIngredients": false, "dontSubtractConsumables": false, "unlockAllShipFeatures": false, "unlockAllShipDecorations": false, diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 173c7221..e519d170 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -17,8 +17,11 @@ import { } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { toOid2 } from "@/src/helpers/inventoryHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { IRecipe } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -46,34 +49,8 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } if (req.query.cancel) { - const inventoryChanges: IInventoryChanges = { - ...updateCurrency(inventory, recipe.buildPrice * -1, false) - }; - - const equipmentIngredients = new Set(); - for (const category of ["LongGuns", "Pistols", "Melee"] as const) { - if (pendingRecipe[category]) { - pendingRecipe[category].forEach(item => { - const index = inventory[category].push(item) - 1; - inventoryChanges[category] ??= []; - inventoryChanges[category].push(inventory[category][index].toJSON()); - equipmentIngredients.add(item.ItemType); - - occupySlot(inventory, InventorySlot.WEAPONS, false); - inventoryChanges.WeaponBin ??= { Slots: 0 }; - inventoryChanges.WeaponBin.Slots -= 1; - }); - } - } - for (const ingredient of recipe.ingredients) { - if (!equipmentIngredients.has(ingredient.ItemType)) { - combineInventoryChanges( - inventoryChanges, - await addItem(inventory, ingredient.ItemType, ingredient.ItemCount) - ); - } - } - + const inventoryChanges: IInventoryChanges = {}; + await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); await inventory.save(); res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. } else { @@ -141,7 +118,43 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = )) }; } + if (config.claimingBlueprintRefundsIngredients) { + await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe); + } await inventory.save(); res.json({ InventoryChanges, BrandedSuits }); } }; + +const refundRecipeIngredients = async ( + inventory: TInventoryDatabaseDocument, + inventoryChanges: IInventoryChanges, + recipe: IRecipe, + pendingRecipe: IPendingRecipeDatabase +): Promise => { + updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges); + + const equipmentIngredients = new Set(); + for (const category of ["LongGuns", "Pistols", "Melee"] as const) { + if (pendingRecipe[category]) { + pendingRecipe[category].forEach(item => { + const index = inventory[category].push(item) - 1; + inventoryChanges[category] ??= []; + inventoryChanges[category].push(inventory[category][index].toJSON()); + equipmentIngredients.add(item.ItemType); + + occupySlot(inventory, InventorySlot.WEAPONS, false); + inventoryChanges.WeaponBin ??= { Slots: 0 }; + inventoryChanges.WeaponBin.Slots -= 1; + }); + } + } + for (const ingredient of recipe.ingredients) { + if (!equipmentIngredients.has(ingredient.ItemType)) { + combineInventoryChanges( + inventoryChanges, + await addItem(inventory, ingredient.ItemType, ingredient.ItemCount) + ); + } + } +}; diff --git a/src/services/configService.ts b/src/services/configService.ts index 26a5008d..8d3206ef 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -24,6 +24,7 @@ interface IConfig { infiniteEndo?: boolean; infiniteRegalAya?: boolean; infiniteHelminthMaterials?: boolean; + claimingBlueprintRefundsIngredients?: boolean; dontSubtractConsumables?: boolean; unlockAllShipFeatures?: boolean; unlockAllShipDecorations?: boolean; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index dd3b229b..cfb3ca16 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1113,24 +1113,29 @@ const isCurrencyTracked = (usePremium: boolean): boolean => { export const updateCurrency = ( inventory: TInventoryDatabaseDocument, price: number, - usePremium: boolean + usePremium: boolean, + inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const currencyChanges: IInventoryChanges = {}; if (price != 0 && isCurrencyTracked(usePremium)) { if (usePremium) { if (inventory.PremiumCreditsFree > 0) { - currencyChanges.PremiumCreditsFree = Math.min(price, inventory.PremiumCreditsFree) * -1; - inventory.PremiumCreditsFree += currencyChanges.PremiumCreditsFree; + const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1; + inventoryChanges.PremiumCreditsFree ??= 0; + inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta; + inventory.PremiumCreditsFree += premiumCreditsFreeDelta; } - currencyChanges.PremiumCredits = -price; - inventory.PremiumCredits += currencyChanges.PremiumCredits; + inventoryChanges.PremiumCredits ??= 0; + inventoryChanges.PremiumCredits -= price; + inventory.PremiumCredits -= price; + logger.debug(`currency changes `, { PremiumCredits: -price }); } else { - currencyChanges.RegularCredits = -price; - inventory.RegularCredits += currencyChanges.RegularCredits; + inventoryChanges.RegularCredits ??= 0; + inventoryChanges.RegularCredits -= price; + inventory.RegularCredits -= price; + logger.debug(`currency changes `, { RegularCredits: -price }); } - logger.debug(`currency changes `, currencyChanges); } - return currencyChanges; + return inventoryChanges; }; export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => { diff --git a/static/webui/index.html b/static/webui/index.html index 2297c8e6..71937337 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -590,6 +590,10 @@
+
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 54f2e147..0edbf98f 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -131,6 +131,7 @@ dict = { cheats_infiniteEndo: `Unendlich Endo`, cheats_infiniteRegalAya: `Unendlich Reines Aya`, cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`, + cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, cheats_dontSubtractConsumables: `Verbrauchsgegenstände (Ausrüstung) nicht verbrauchen`, cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`, cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index da0d5a0d..9dfeb21a 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -130,6 +130,7 @@ dict = { cheats_infiniteEndo: `Infinite Endo`, cheats_infiniteRegalAya: `Infinite Regal Aya`, cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`, + cheats_claimingBlueprintRefundsIngredients: `Claiming Blueprint Refunds Ingredients`, cheats_dontSubtractConsumables: `Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Unlock All Ship Features`, cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 3caa2b83..8251a1c9 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -131,6 +131,7 @@ dict = { cheats_infiniteEndo: `Endo infinito`, cheats_infiniteRegalAya: `Aya Real infinita`, cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`, + cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, cheats_dontSubtractConsumables: `No restar consumibles`, cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`, cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 9f35699d..9253bfff 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -131,6 +131,7 @@ dict = { cheats_infiniteEndo: `Endo infini`, cheats_infiniteRegalAya: `Aya Raffiné infini`, cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`, + cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`, cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 3861d7ff..a2bb6354 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -131,6 +131,7 @@ dict = { cheats_infiniteEndo: `Бесконечное эндо`, cheats_infiniteRegalAya: `Бесконечная Королевская Айя`, cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`, + cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, cheats_dontSubtractConsumables: `Не уменьшать количество расходников`, cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`, cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index dacba31e..a242891a 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -131,6 +131,7 @@ dict = { cheats_infiniteEndo: `无限内融核心`, cheats_infiniteRegalAya: `无限御品阿耶`, cheats_infiniteHelminthMaterials: `无限Helminth材料`, + cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, cheats_unlockAllShipFeatures: `解锁所有飞船功能`, cheats_unlockAllShipDecorations: `解锁所有飞船装饰`, From c83e732b88ba0b4b6ff78bd1328ee56c9447c252 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:37:59 -0700 Subject: [PATCH 731/776] feat: gifting bonus (#2036) Closes #2014 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2036 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/giftingController.ts | 31 +++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts index 0d5c4723..09b48f4e 100644 --- a/src/controllers/api/giftingController.ts +++ b/src/controllers/api/giftingController.ts @@ -2,12 +2,13 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { Account } from "@/src/models/loginModel"; import { areFriends } from "@/src/services/friendService"; import { createMessage } from "@/src/services/inboxService"; -import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { IOid } from "@/src/types/commonTypes"; -import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; -import { ExportFlavour } from "warframe-public-export-plus"; +import { ExportBundles, ExportFlavour } from "warframe-public-export-plus"; export const giftingController: RequestHandler = async (req, res) => { const data = getJSONfromString(String(req.body)); @@ -44,10 +45,7 @@ export const giftingController: RequestHandler = async (req, res) => { // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20) - const senderInventory = await getInventory( - senderAccount._id.toString(), - "PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining" - ); + const senderInventory = await getInventory(senderAccount._id.toString()); if (senderInventory.GiftsRemaining == 0) { res.status(400).send("10").end(); @@ -55,7 +53,20 @@ export const giftingController: RequestHandler = async (req, res) => { } senderInventory.GiftsRemaining -= 1; - updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true); + const inventoryChanges: IInventoryChanges = updateCurrency( + senderInventory, + data.PurchaseParams.ExpectedPrice, + true + ); + if (data.PurchaseParams.StoreItem in ExportBundles) { + const bundle = ExportBundles[data.PurchaseParams.StoreItem]; + if (bundle.giftingBonus) { + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges + ); + } + } await senderInventory.save(); const senderName = getSuffixedName(senderAccount); @@ -83,7 +94,9 @@ export const giftingController: RequestHandler = async (req, res) => { } ]); - res.end(); + res.json({ + InventoryChanges: inventoryChanges + }); }; interface IGiftingRequest { From 9d4bce852e9c4633f82ad76008d84a670d1d48ea Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:38:13 -0700 Subject: [PATCH 732/776] feat: the circuit (#2039) Closes #1965 Closes #2041 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2039 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/endlessXpController.ts | 569 ++++++++++++++++-- src/models/inventoryModels/inventoryModel.ts | 47 +- src/services/importService.ts | 3 - src/services/missionInventoryUpdateService.ts | 40 +- src/types/inventoryTypes/inventoryTypes.ts | 22 +- 5 files changed, 619 insertions(+), 62 deletions(-) diff --git a/src/controllers/api/endlessXpController.ts b/src/controllers/api/endlessXpController.ts index 656ebb05..1be4bd2b 100644 --- a/src/controllers/api/endlessXpController.ts +++ b/src/controllers/api/endlessXpController.ts @@ -1,60 +1,529 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; +import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus"; +import { getRandomElement } from "@/src/services/rngService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const endlessXpController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const payload = getJSONfromString(String(req.body)); - - inventory.EndlessXP ??= []; - const entry = inventory.EndlessXP.find(x => x.Category == payload.Category); - if (entry) { - entry.Choices = payload.Choices; - } else { - inventory.EndlessXP.push({ - Category: payload.Category, - Choices: payload.Choices - }); - } - await inventory.save(); - - res.json({ - NewProgress: { - Category: payload.Category, - Earn: 0, - Claim: 0, - BonusAvailable: { - $date: { - $numberLong: "9999999999999" - } - }, - Expiry: { - $date: { - $numberLong: "9999999999999" - } - }, - Choices: payload.Choices, - PendingRewards: [ - { - RequiredTotalXp: 190, - Rewards: [ - { - StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", - ItemCount: 1 - } - ] - } - // ... - ] + if (payload.Mode == "r") { + const inventory = await getInventory(accountId, "EndlessXP"); + inventory.EndlessXP ??= []; + let entry = inventory.EndlessXP.find(x => x.Category == payload.Category); + if (!entry) { + entry = { + Category: payload.Category, + Earn: 0, + Claim: 0, + Choices: payload.Choices, + PendingRewards: [] + }; + inventory.EndlessXP.push(entry); } - }); + + const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000; + const weekEnd = weekStart + 604800000; + + entry.Earn = 0; + entry.Claim = 0; + entry.BonusAvailable = new Date(weekStart); + entry.Expiry = new Date(weekEnd); + entry.Choices = payload.Choices; + entry.PendingRewards = + payload.Category == "EXC_HARD" + ? generateHardModeRewards(payload.Choices) + : generateNormalModeRewards(payload.Choices); + + await inventory.save(); + res.json({ + NewProgress: inventory.toJSON().EndlessXP!.find(x => x.Category == payload.Category)! + }); + } else if (payload.Mode == "c") { + const inventory = await getInventory(accountId); + const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!; + const inventoryChanges: IInventoryChanges = {}; + for (const reward of entry.PendingRewards) { + if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) { + combineInventoryChanges( + inventoryChanges, + ( + await handleStoreItemAcquisition( + reward.Rewards[0].StoreItem, + inventory, + reward.Rewards[0].ItemCount + ) + ).InventoryChanges + ); + } + } + entry.Claim = entry.Earn; + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + ClaimedXp: entry.Claim + }); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unexpected endlessXp mode: ${payload.Mode}`); + } }; -interface IEndlessXpRequest { - Mode: string; // "r" - Category: TEndlessXpCategory; - Choices: string[]; -} +type IEndlessXpRequest = + | { + Mode: "r"; + Category: TEndlessXpCategory; + Choices: string[]; + } + | { + Mode: "c" | "something else"; + Category: TEndlessXpCategory; + }; + +const generateRandomRewards = (deckName: string): ICountedStoreItem[] => { + const reward = getRandomElement(ExportRewards[deckName][0])!; + return [ + { + StoreItem: reward.type, + ItemCount: reward.itemCount + } + ]; +}; + +const normalModeChosenRewards: Record = { + Excalibur: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint" + ], + Trinity: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint" + ], + Ember: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint" + ], + Loki: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint" + ], + Mag: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint" + ], + Rhino: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint" + ], + Ash: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint" + ], + Frost: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint" + ], + Nyx: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint" + ], + Saryn: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint" + ], + Vauban: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint" + ], + Nova: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint" + ], + Nekros: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint" + ], + Valkyr: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint" + ], + Oberon: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint" + ], + Hydroid: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint" + ], + Mirage: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint" + ], + Limbo: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint" + ], + Mesa: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint" + ], + Chroma: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint" + ], + Atlas: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint" + ], + Ivara: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint" + ], + Inaros: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint" + ], + Titania: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint" + ], + Nidus: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint" + ], + Octavia: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint" + ], + Harrow: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint" + ], + Gara: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint" + ], + Khora: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint" + ], + Revenant: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint" + ], + Garuda: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint" + ], + Baruuk: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint" + ], + Hildryn: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint", + "/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint" + ] +}; + +const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => { + const choiceRewards = normalModeChosenRewards[choices[0]]; + return [ + { + RequiredTotalXp: 190, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards" + ) + }, + { + RequiredTotalXp: 400, + Rewards: [ + { + StoreItem: choiceRewards[0], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 630, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards" + ) + }, + { + RequiredTotalXp: 890, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards" + ) + }, + { + RequiredTotalXp: 1190, + Rewards: [ + { + StoreItem: choiceRewards[1], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 1540, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards" + ) + }, + { + RequiredTotalXp: 1950, + Rewards: [ + { + StoreItem: choiceRewards[2], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 2430, + Rewards: [ + { + StoreItem: choiceRewards[3], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 2990, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards" + ) + }, + { + RequiredTotalXp: 3640, + Rewards: [ + { + StoreItem: choiceRewards[4], + ItemCount: 1 + } + ] + } + ]; +}; + +const hardModeChosenRewards: Record = { + Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker", + Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker", + Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker", + Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker", + Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker", + Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker", + Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker", + Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker", + Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker", + Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker", + Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker", + Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker", + Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker", + Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker", + Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker", + Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker", + Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker", + Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker", + Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker", + CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker", + Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker", + DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker", + DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker", + Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker", + Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker", + AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker", + Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker", + Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker", + NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker", + Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker", + Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker", + Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker", + Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker", + Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker", + Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker", + Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker", + Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker", + Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker", + Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker", + Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker", + RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod", + RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod", + RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod", + Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward" +}; + +const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => { + return [ + { + RequiredTotalXp: 285, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards" + ) + }, + { + RequiredTotalXp: 600, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards" + ) + }, + { + RequiredTotalXp: 945, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards" + ) + }, + { + RequiredTotalXp: 1335, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards" + ) + }, + { + RequiredTotalXp: 1785, + Rewards: [ + { + StoreItem: hardModeChosenRewards[choices[0]], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 2310, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards" + ) + }, + { + RequiredTotalXp: 2925, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards" + ) + }, + { + RequiredTotalXp: 3645, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards" + ) + }, + { + RequiredTotalXp: 4485, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards" + ) + }, + { + RequiredTotalXp: 5460, + Rewards: [ + { + StoreItem: hardModeChosenRewards[choices[1]], + ItemCount: 1 + } + ] + } + ]; +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5525b500..ba9a1010 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -38,7 +38,8 @@ import { IPeriodicMissionCompletionResponse, ILoreFragmentScan, IEvolutionProgress, - IEndlessXpProgress, + IEndlessXpProgressDatabase, + IEndlessXpProgressClient, ICrewShipCustomization, ICrewShipWeapon, ICrewShipWeaponEmplacements, @@ -97,7 +98,8 @@ import { IInvasionProgressClient, IAccolades, IHubNpcCustomization, - ILotusCustomization + ILotusCustomization, + IEndlessXpReward } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -112,6 +114,7 @@ import { } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel"; +import { ICountedStoreItem } from "warframe-public-export-plus"; export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); @@ -810,14 +813,48 @@ const evolutionProgressSchema = new Schema( { _id: false } ); -const endlessXpProgressSchema = new Schema( +const countedStoreItemSchema = new Schema( { - Category: String, - Choices: [String] + StoreItem: String, + ItemCount: Number }, { _id: false } ); +const endlessXpRewardSchema = new Schema( + { + RequiredTotalXp: Number, + Rewards: [countedStoreItemSchema] + }, + { _id: false } +); + +const endlessXpProgressSchema = new Schema( + { + Category: { type: String, required: true }, + Earn: { type: Number, default: 0 }, + Claim: { type: Number, default: 0 }, + BonusAvailable: Date, + Expiry: Date, + Choices: { type: [String], required: true }, + PendingRewards: { type: [endlessXpRewardSchema], default: [] } + }, + { _id: false } +); + +endlessXpProgressSchema.set("toJSON", { + transform(_doc, ret) { + const db = ret as IEndlessXpProgressDatabase; + const client = ret as IEndlessXpProgressClient; + + if (db.BonusAvailable) { + client.BonusAvailable = toMongoDate(db.BonusAvailable); + } + if (db.Expiry) { + client.Expiry = toMongoDate(db.Expiry); + } + } +}); const crewShipWeaponEmplacementsSchema = new Schema( { PRIMARY_A: EquipmentSelectionSchema, diff --git a/src/services/importService.ts b/src/services/importService.ts index 7fb88619..3f7f0051 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -377,9 +377,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< db[key] = client[key]; } } - if (client.EndlessXP !== undefined) { - db.EndlessXP = client.EndlessXP; - } if (client.SongChallenges !== undefined) { db.SongChallenges = client.SongChallenges; } diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a7172bf6..a2d76ae1 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -10,7 +10,7 @@ import { import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService"; -import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { equipmentKeys, IMission, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addBooster, addChallenges, @@ -841,7 +841,13 @@ export const addMissionRewards = async ( } //TODO: check double reward merging - const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion); + const MissionRewards: IMissionReward[] = getRandomMissionDrops( + inventory, + rewardInfo, + missions, + wagerTier, + firstCompletion + ); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; const AffiliationMods: IAffiliationMods[] = []; @@ -1290,6 +1296,7 @@ function getLevelCreditRewards(node: IRegion): number { function getRandomMissionDrops( inventory: TInventoryDatabaseDocument, RewardInfo: IRewardInfo, + mission: IMission | undefined, tierOverride: number | undefined, firstCompletion: boolean ): IMissionReward[] { @@ -1531,6 +1538,35 @@ function getRandomMissionDrops( logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`); } } else { + if (RewardInfo.node == "SolNode238") { + // The Circuit + const category = mission?.Tier == 1 ? "EXC_HARD" : "EXC_NORMAL"; + const progress = inventory.EndlessXP?.find(x => x.Category == category); + if (progress) { + // https://wiki.warframe.com/w/The%20Circuit#Tiers_and_Weekly_Rewards + const roundsCompleted = RewardInfo.rewardQualifications?.length || 0; + if (roundsCompleted >= 1) { + progress.Earn += 100; + } + if (roundsCompleted >= 2) { + progress.Earn += 110; + } + if (roundsCompleted >= 3) { + progress.Earn += 125; + } + if (roundsCompleted >= 4) { + progress.Earn += 145; + if (progress.BonusAvailable && progress.BonusAvailable.getTime() <= Date.now()) { + progress.Earn += 50; + progress.BonusAvailable = new Date(Date.now() + 24 * 3600_000); // TOVERIFY + } + } + if (roundsCompleted >= 5) { + progress.Earn += (roundsCompleted - 4) * 170; + } + } + tierOverride = 0; + } rotations = getRotations(RewardInfo, tierOverride); } if (rewardManifests.length != 0) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index a32fc796..3ca59ffd 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -12,6 +12,7 @@ import { } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper"; import { IOrbiter } from "../personalRoomsTypes"; +import { ICountedStoreItem } from "warframe-public-export-plus"; export type InventoryDatabaseEquipment = { [_ in TEquipmentKey]: IEquipmentDatabase[]; @@ -54,6 +55,7 @@ export interface IInventoryDatabase | "CrewMembers" | "QualifyingInvasions" | "LastInventorySync" + | "EndlessXP" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -92,6 +94,7 @@ export interface IInventoryDatabase CrewMembers: ICrewMemberDatabase[]; QualifyingInvasions: IInvasionProgressDatabase[]; LastInventorySync?: Types.ObjectId; + EndlessXP?: IEndlessXpProgressDatabase[]; } export interface IQuestKeyDatabase { @@ -356,7 +359,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu PendingCoupon?: IPendingCouponClient; Harvestable: boolean; DeathSquadable: boolean; - EndlessXP?: IEndlessXpProgress[]; + EndlessXP?: IEndlessXpProgressClient[]; DialogueHistory?: IDialogueHistoryClient; CalendarProgress?: ICalendarProgress; SongChallenges?: ISongChallenge[]; @@ -1143,9 +1146,24 @@ export interface IEvolutionProgress { export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD"; -export interface IEndlessXpProgress { +export interface IEndlessXpProgressDatabase { Category: TEndlessXpCategory; + Earn: number; + Claim: number; + BonusAvailable?: Date; + Expiry?: Date; Choices: string[]; + PendingRewards: IEndlessXpReward[]; +} + +export interface IEndlessXpProgressClient extends Omit { + BonusAvailable?: IMongoDate; + Expiry?: IMongoDate; +} + +export interface IEndlessXpReward { + RequiredTotalXp: number; + Rewards: ICountedStoreItem[]; } export interface IDialogueHistoryClient { From b451c73598e31aad28566209ed7e789f7f1ce85c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 9 May 2025 21:38:42 -0700 Subject: [PATCH 733/776] chore: handle mods picked up in mission on U19 (#2042) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2042 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/artifactTransmutationController.ts | 24 ++++++------------- src/services/missionInventoryUpdateService.ts | 10 ++++++-- src/types/inventoryTypes/inventoryTypes.ts | 12 +++++++++- src/types/requestTypes.ts | 4 ++-- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts index 4310c7c4..deb05cde 100644 --- a/src/controllers/api/artifactTransmutationController.ts +++ b/src/controllers/api/artifactTransmutationController.ts @@ -1,9 +1,9 @@ -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; -import { IOid } from "@/src/types/commonTypes"; +import { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus"; @@ -24,7 +24,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res) ]); payload.Consumed.forEach(upgrade => { - inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid }); + inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); }); const rawRivenType = getRandomRawRivenType(); @@ -57,8 +57,8 @@ export const artifactTransmutationController: RequestHandler = async (req, res) payload.Consumed.forEach(upgrade => { const meta = ExportUpgrades[upgrade.ItemType]; counts[meta.rarity] += upgrade.ItemCount; - if (upgrade.ItemId.$oid != "000000000000000000000000") { - inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid }); + if (fromOid(upgrade.ItemId) != "000000000000000000000000") { + inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); } else { addMods(inventory, [ { @@ -128,24 +128,14 @@ const getRandomRawRivenType = (): string => { }; interface IArtifactTransmutationRequest { - Upgrade: IAgnosticUpgradeClient; + Upgrade: IUpgradeFromClient; LevelDiff: number; - Consumed: IAgnosticUpgradeClient[]; + Consumed: IUpgradeFromClient[]; Cost: number; FusionPointCost: number; RivenTransmute?: boolean; } -interface IAgnosticUpgradeClient { - ItemType: string; - ItemId: IOid; - FromSKU: boolean; - UpgradeFingerprint: string; - PendingRerollFingerprint: string; - ItemCount: number; - LastAdded: IOid; -} - const specialModSets: string[][] = [ [ "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a2d76ae1..14d700ff 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -400,8 +400,14 @@ export const addMissionInventoryUpdates = async ( break; case "Upgrades": value.forEach(clientUpgrade => { - const upgrade = inventory.Upgrades.id(fromOid(clientUpgrade.ItemId))!; - upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress + const id = fromOid(clientUpgrade.ItemId); + if (id == "") { + // U19 does not provide RawUpgrades and instead interleaves them with riven progress here + addMods(inventory, [clientUpgrade]); + } else { + const upgrade = inventory.Upgrades.id(id)!; + upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress + } }); break; case "WeaponSkins": diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 3ca59ffd..b18696cc 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -535,6 +535,16 @@ export interface IUpgradeDatabase extends Omit { _id: Types.ObjectId; } +export interface IUpgradeFromClient { + ItemType: string; + ItemId: IOidWithLegacySupport; + FromSKU?: boolean; + UpgradeFingerprint: string; + PendingRerollFingerprint: string; + ItemCount: number; + LastAdded: IOidWithLegacySupport; +} + export interface ICrewShipMembersClient { SLOT_A?: ICrewShipMemberClient; SLOT_B?: ICrewShipMemberClient; @@ -1053,7 +1063,7 @@ export interface IQuestStage { export interface IRawUpgrade { ItemType: string; ItemCount: number; - LastAdded?: IOid; + LastAdded?: IOidWithLegacySupport; } export interface ISeasonChallenge { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index e47b5b86..ab895bd1 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -15,7 +15,7 @@ import { IPlayerSkills, IQuestKeyDatabase, ILoreFragmentScan, - IUpgradeClient, + IUpgradeFromClient, ICollectibleEntry, IDiscoveredMarker, ILockedWeaponGroupClient, @@ -111,7 +111,7 @@ export type IMissionInventoryUpdateRequest = { Standing: number; }[]; CollectibleScans?: ICollectibleEntry[]; - Upgrades?: IUpgradeClient[]; // riven challenge progress + Upgrades?: IUpgradeFromClient[]; // riven challenge progress WeaponSkins?: IWeaponSkinClient[]; StrippedItems?: { DropTable: string; From 5f9ae2aef654311c41081d50b5637b3093a5a458 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Sat, 10 May 2025 03:44:42 -0700 Subject: [PATCH 734/776] chore(webui): update to Spanish translation (#2045) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2045 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 8251a1c9..83cdc5fb 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -131,7 +131,7 @@ dict = { cheats_infiniteEndo: `Endo infinito`, cheats_infiniteRegalAya: `Aya Real infinita`, cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`, - cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, + cheats_claimingBlueprintRefundsIngredients: `Reclamar ingredientes devueltos por planos`, cheats_dontSubtractConsumables: `No restar consumibles`, cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`, cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`, From b201508fa133f5dfc931f13a4e0939a3458b4118 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Sat, 10 May 2025 03:44:55 -0700 Subject: [PATCH 735/776] chore(webui): update German translation (#2046) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2046 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 0edbf98f..156063d2 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -131,7 +131,7 @@ dict = { cheats_infiniteEndo: `Unendlich Endo`, cheats_infiniteRegalAya: `Unendlich Reines Aya`, cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`, - cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, + cheats_claimingBlueprintRefundsIngredients: `Fertige Blaupausen erstatten Ressourcen zurück`, cheats_dontSubtractConsumables: `Verbrauchsgegenstände (Ausrüstung) nicht verbrauchen`, cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`, cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`, From 5bc39aac8aca61229897b19cb49b5094486531b5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 10 May 2025 19:12:17 -0700 Subject: [PATCH 736/776] fix: login failure on U22.8 (#2044) 2018.01.04.13.12 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2044 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/routes/api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/api.ts b/src/routes/api.ts index 1ce340b8..82b42842 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -187,6 +187,7 @@ apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); apiRouter.get("/getShip.php", getShipController); +apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8 apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/hub", hubController); apiRouter.get("/hubInstances", hubInstancesController); From d5297d35477d694547b935abfe174ffe844d59c4 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 10 May 2025 19:12:25 -0700 Subject: [PATCH 737/776] fix(webui): sidebar toggler not showing up on small screens (#2053) Closes #2049 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2053 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/index.html | 6 +++--- static/webui/style.css | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/static/webui/index.html b/static/webui/index.html index 71937337..fae24828 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -7,9 +7,9 @@ -