From a27f1c5e017dcbcd1527138b5629b188d753f1c4 Mon Sep 17 00:00:00 2001 From: VampireKitten Date: Tue, 25 Feb 2025 10:08:27 -0800 Subject: [PATCH 001/354] 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 002/354] 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 003/354] 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 004/354] 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 005/354] 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 006/354] 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 007/354] 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 008/354] 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 009/354] 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 010/354] 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 011/354] 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 012/354] 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 013/354] 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 014/354] 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 015/354] 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 016/354] 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 017/354] 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 018/354] 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 019/354] 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 020/354] 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 021/354] 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 022/354] 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 023/354] 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 024/354] 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 025/354] 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 026/354] 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 027/354] 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 028/354] 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 029/354] 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 030/354] 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 031/354] 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 032/354] 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 033/354] 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 034/354] 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 035/354] 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 036/354] 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 037/354] 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 038/354] 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 039/354] 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 040/354] 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 041/354] 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 042/354] 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 043/354] 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 044/354] 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 045/354] 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 046/354] 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 047/354] 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 048/354] 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 049/354] 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 050/354] 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 051/354] 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 052/354] 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 053/354] 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 054/354] 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 055/354] 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 056/354] 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 057/354] 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 058/354] 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 059/354] 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 060/354] 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 061/354] 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 062/354] 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 063/354] 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 064/354] 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 065/354] 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 066/354] 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 067/354] 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 068/354] 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 069/354] 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 070/354] 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 071/354] 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 072/354] 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 073/354] 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 074/354] 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 075/354] 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 076/354] 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 077/354] 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 078/354] 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 079/354] 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 080/354] 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 081/354] 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 082/354] 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 083/354] 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 084/354] 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 085/354] 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 086/354] 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 087/354] 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 088/354] 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 089/354] 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 090/354] 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 091/354] 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 092/354] 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 093/354] 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 094/354] 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 095/354] 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 096/354] 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 097/354] 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 098/354] 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 099/354] 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 100/354] 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 101/354] 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 102/354] 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 103/354] 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 104/354] 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 105/354] 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 106/354] 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 107/354] 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 108/354] 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 109/354] 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 110/354] 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 111/354] 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 112/354] 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 113/354] 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 114/354] 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 115/354] 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 116/354] 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 117/354] 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 118/354] 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 119/354] 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 120/354] 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 121/354] 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 122/354] 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 123/354] 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 124/354] 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 125/354] 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 126/354] 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 127/354] 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 128/354] 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 129/354] 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 130/354] 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 131/354] 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 132/354] 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 133/354] 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 134/354] 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 135/354] 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 136/354] 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 137/354] 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 138/354] 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 139/354] 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 140/354] 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 141/354] 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 142/354] 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 143/354] 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 144/354] 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 145/354] 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 146/354] 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 147/354] 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 148/354] 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 149/354] 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 150/354] 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 151/354] 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 152/354] 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 153/354] 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 154/354] 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 155/354] 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 156/354] 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 157/354] 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 158/354] 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 159/354] 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 160/354] 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 161/354] 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 162/354] 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 163/354] 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 164/354] 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 165/354] 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 166/354] 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 167/354] 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 168/354] 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 169/354] 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 170/354] 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 171/354] 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 172/354] 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 173/354] 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 174/354] 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 175/354] 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 176/354] 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 177/354] 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 178/354] 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 179/354] 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 180/354] 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 181/354] 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 182/354] 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 183/354] 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 184/354] 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 185/354] 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 186/354] 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 187/354] 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 188/354] 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 189/354] 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 190/354] 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 191/354] 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 192/354] 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 193/354] 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 194/354] 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 195/354] 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 196/354] 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 197/354] 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 198/354] 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 199/354] 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 200/354] 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 201/354] 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 202/354] 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 203/354] 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 204/354] 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 205/354] 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 206/354] 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 207/354] 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 208/354] 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 209/354] 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 210/354] 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 211/354] 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 212/354] 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 213/354] 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 214/354] 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 215/354] 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 216/354] 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 217/354] 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 218/354] 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 219/354] 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 220/354] 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 221/354] 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 222/354] 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 223/354] 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 224/354] 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 225/354] 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 226/354] 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 227/354] 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 228/354] 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 229/354] 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 230/354] 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 231/354] 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 232/354] 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 233/354] 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 234/354] 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 235/354] 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 236/354] 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 237/354] 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 238/354] 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 239/354] 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 240/354] 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 241/354] 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 242/354] 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 243/354] 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 244/354] 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 245/354] 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 246/354] 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 247/354] 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 248/354] 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 249/354] 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 250/354] 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 251/354] 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 252/354] 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 253/354] 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 254/354] 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 255/354] 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 256/354] 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 257/354] 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 258/354] 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 259/354] 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 260/354] 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 261/354] 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 262/354] 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 263/354] 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 264/354] 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 265/354] 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 266/354] 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 267/354] 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 268/354] 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 269/354] 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 270/354] 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 271/354] 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 272/354] 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 273/354] 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 274/354] 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 275/354] 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 276/354] 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 277/354] 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 278/354] 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 279/354] 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 280/354] 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 281/354] 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 282/354] 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 283/354] 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 284/354] 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 285/354] 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 286/354] 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 287/354] 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 288/354] 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 289/354] 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 290/354] 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 291/354] 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 292/354] 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 293/354] 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 294/354] 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 295/354] 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 296/354] 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 297/354] 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 298/354] 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 299/354] 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 300/354] 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 301/354] 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 302/354] 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 303/354] 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 304/354] 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 305/354] 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 306/354] 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 307/354] 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 308/354] 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 309/354] 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 310/354] 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 311/354] 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 312/354] 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 313/354] 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 314/354] 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 315/354] 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 316/354] 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 317/354] 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 318/354] 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 319/354] 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 320/354] 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 321/354] 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 322/354] 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 323/354] 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 324/354] 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 325/354] 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 326/354] 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 327/354] 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 328/354] 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 329/354] 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 330/354] 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 331/354] 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 332/354] 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 333/354] 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 334/354] 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 335/354] 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 336/354] 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 337/354] 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 338/354] 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 339/354] 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 340/354] 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 341/354] 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 342/354] 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 343/354] 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 344/354] 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 345/354] 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 346/354] 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 347/354] 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 348/354] 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 349/354] 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 350/354] 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 351/354] 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 352/354] 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 353/354] 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 354/354] 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 }; };