From a47eccdec8b85d30b6ddfcaa6c515db76fc8fab9 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 21 May 2025 04:51:36 -0700 Subject: [PATCH 01/30] fix: provide response for FocusOperation.ActivateWay (#2092) Closes #2091 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2092 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/focusController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index de892829..90b55a2e 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -64,7 +64,9 @@ export const focusController: RequestHandler = async (req, res) => { } ); - res.end(); + res.json({ + FocusUpgrade: { ItemType: focusType } + }); break; } case FocusOperation.UnlockUpgrade: { From 09b9683fa1e499620ecf2a6aae62bf7fa46f607f Mon Sep 17 00:00:00 2001 From: hxedcl Date: Wed, 21 May 2025 22:16:00 -0700 Subject: [PATCH 02/30] chore(webui): update to Spanish translation (#2095) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2095 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 80db0dbf..652a850c 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -151,7 +151,7 @@ dict = { cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, cheats_noDeathMarks: `Sin marcas de muerte`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, - cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`, + cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`, cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, From 92d34fd69ef2ea13da27ddcd58d586baf41dce5a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 21 May 2025 22:16:44 -0700 Subject: [PATCH 03/30] chore: update shard removal costs for 38.6.0 (#2094) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2094 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/infestedFoundryController.ts | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index 28e89adc..4cc21061 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService"; import { IOid } from "@/src/types/commonTypes"; @@ -12,7 +12,7 @@ import { } from "@/src/types/inventoryTypes/inventoryTypes"; import { ExportMisc } from "warframe-public-export-plus"; import { getRecipe } from "@/src/services/itemDataService"; -import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; import { colorToShard } from "@/src/helpers/shardHelper"; import { config } from "@/src/services/configService"; @@ -23,12 +23,12 @@ import { } from "@/src/services/infestedFoundryService"; export const infestedFoundryController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); switch (req.query.mode) { case "s": { // shard installation const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); 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) => { case "x": { // shard removal const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const suit = inventory.Suits.id(request.SuitId.$oid)!; const miscItemChanges: IMiscItem[] = []; @@ -70,19 +70,30 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { ItemCount: 1 }); addMiscItems(inventory, miscItemChanges); + + // consume resources + if (!config.infiniteHelminthMaterials) { + let type: string; + let count: number; + if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) { + // < 38.6.0 + type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile"; + count = 300; + } else { + // >= 38.6.0 + type = + archonCrystalRemovalResource[ + suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "") + ]; + count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150; + } + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count; + } } // remove from suit suit.ArchonCrystalUpgrades![request.Slot] = {}; - if (!config.infiniteHelminthMaterials) { - // remove bile - const bile = inventory.InfestedFoundry!.Resources!.find( - x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile" - )!; - bile.Count -= 300; - } - await inventory.save(); const infestedFoundry = inventory.toJSON().InfestedFoundry!; @@ -99,7 +110,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "n": { // name the beast const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.Name = request.newName; await inventory.save(); @@ -122,7 +133,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { } const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.Resources ??= []; @@ -218,7 +229,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "o": { // offerings update const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex; inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes; @@ -239,7 +250,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "a": { // subsume warframe const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const recipe = getRecipe(request.Recipe)!; if (!config.infiniteHelminthMaterials) { for (const ingredient of recipe.secretIngredients!) { @@ -289,7 +300,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "r": { // rush subsume - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const currencyChanges = updateCurrency(inventory, 50, true); const recipeChanges = handleSubsumeCompletion(inventory); await inventory.save(); @@ -307,7 +318,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "u": { const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const suit = inventory.Suits.id(request.SuitId.$oid)!; const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); suit.OffensiveUpgrade = request.OffensiveUpgradeType; @@ -340,7 +351,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { } case "custom_unlockall": { - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.XP ??= 0; if (151875_00 > inventory.InfestedFoundry.XP) { @@ -439,3 +450,12 @@ const apetiteModel = (x: number): number => { } return 3; }; + +const archonCrystalRemovalResource: Record = { + ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", + ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", + ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", + ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", + ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", + ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx" +}; From ba6cd47432c59791def15c27b08a8aed31874d71 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 23 May 2025 06:12:54 -0700 Subject: [PATCH 04/30] feat: initial support for multiple nightwave seasons (#2096) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2096 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/missionInventoryUpdateController.ts | 2 +- .../api/syndicateSacrificeController.ts | 42 +- .../api/updateChallengeProgressController.ts | 16 +- src/services/inventoryService.ts | 48 +- src/services/missionInventoryUpdateService.ts | 4 +- src/services/serversideVendorsService.ts | 2 - src/services/worldStateService.ts | 125 +- src/types/worldStateTypes.ts | 2 +- ...dioLegionIntermission12VendorManifest.json | 1192 ----------------- 11 files changed, 150 insertions(+), 1293 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json diff --git a/package-lock.json b/package-lock.json index 9ab7b4bd..c1dea433 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.62", + "warframe-public-export-plus": "^0.5.64", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3703,9 +3703,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.62", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz", - "integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA==" + "version": "0.5.64", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.64.tgz", + "integrity": "sha512-JyHRtYumfwQ1Iog2unzlBWfQHJlZER+iUISquyFFv0Qqtv2QsNzFv2AbV7sCaqgDcE8tw6e5/YqGgfI0m403/g==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 5b995cbd..272736d2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.62", + "warframe-public-export-plus": "^0.5.64", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index fedff108..3b5009c2 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -57,7 +57,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) const firstCompletion = missionReport.SortieId ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 : false; - const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); + const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport); if ( missionReport.MissionStatus !== "GS_SUCCESS" && diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index 2dc1d67d..8b1a9c7a 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -1,15 +1,13 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; +import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { isStoreItem, toStoreItem } from "@/src/services/itemDataService"; +import { toStoreItem } from "@/src/services/itemDataService"; import { logger } from "@/src/utils/logger"; -const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName; - export const syndicateSacrificeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); const inventory = await getInventory(accountId); @@ -54,13 +52,6 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp syndicate.Title ??= 0; syndicate.Title += 1; - if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) { - syndicate.FreeFavorsEarned ??= []; - if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { - syndicate.FreeFavorsEarned.push(syndicate.Title); - } - } - if (reward) { combineInventoryChanges( res.InventoryChanges, @@ -68,24 +59,37 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp ); } - if (data.AffiliationTag == ExportNightwave.affiliationTag) { - const index = syndicate.Title - 1; - if (index < ExportNightwave.rewards.length) { + // Quacks like a nightwave syndicate? + if (manifest.dailyChallenges) { + const title = manifest.titles!.find(x => x.level == syndicate.Title); + if (title) { res.NewEpisodeReward = true; - const reward = ExportNightwave.rewards[index]; - let rewardType = reward.uniqueName; - if (!isStoreItem(rewardType)) { - rewardType = toStoreItem(rewardType); + let rewardType: string; + let rewardCount: number; + if (title.storeItemReward) { + rewardType = title.storeItemReward; + rewardCount = 1; + } else { + rewardType = toStoreItem(title.reward!.ItemType); + rewardCount = title.reward!.ItemCount; } - const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)) + const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) .InventoryChanges; if (Object.keys(rewardInventoryChanges).length == 0) { logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); + const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }]; addMiscItems(inventory, rewardInventoryChanges.MiscItems); } combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); } + } else { + if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) { + syndicate.FreeFavorsEarned ??= []; + if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { + syndicate.FreeFavorsEarned.push(syndicate.Title); + } + } } await inventory.save(); diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index 93135634..b948bb79 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -1,18 +1,26 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; 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 account = await getAccountForRequest(req); - const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations"); + const inventory = await getInventory( + account._id.toString(), + "ChallengeProgress SeasonChallengeHistory Affiliations" + ); let affiliationMods: IAffiliationMods[] = []; if (challenges.ChallengeProgress) { - affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions); + affiliationMods = addChallenges( + account, + inventory, + challenges.ChallengeProgress, + challenges.SeasonChallengeCompletions + ); } if (challenges.SeasonChallengeHistory) { challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 09d23306..4ccd9c5c 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -44,6 +44,7 @@ import { import { ExportArcanes, ExportBundles, + ExportChallenges, ExportCustoms, ExportDrones, ExportEmailItems, @@ -53,7 +54,6 @@ import { ExportGear, ExportKeys, ExportMisc, - ExportNightwave, ExportRailjackWeapons, ExportRecipes, ExportResources, @@ -83,8 +83,9 @@ import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService"; import { createMessage } from "./inboxService"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; -import { getWorldState } from "./worldStateService"; +import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService"; import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; +import { TAccountDocument } from "./loginService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -1716,6 +1717,7 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: }; export const addChallenges = ( + account: TAccountDocument, inventory: TInventoryDatabaseDocument, ChallengeProgress: IChallengeProgress[], SeasonChallengeCompletions: ISeasonChallenge[] | undefined @@ -1738,26 +1740,32 @@ export const addChallenges = ( continue; } - const meta = ExportNightwave.challenges[challenge.challenge]; - logger.debug("Completed challenge", meta); + const meta = ExportChallenges[challenge.challenge]; + const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel); + logger.debug("Completed season challenge", { + uniqueName: challenge.challenge, + syndicateTag: nightwaveSyndicateTag, + ...meta + }); + if (nightwaveSyndicateTag) { + let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag); + if (!affiliation) { + affiliation = + inventory.Affiliations[ + inventory.Affiliations.push({ + Tag: nightwaveSyndicateTag, + Standing: 0 + }) - 1 + ]; + } + affiliation.Standing += meta.standing!; - let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag); - if (!affiliation) { - affiliation = - inventory.Affiliations[ - inventory.Affiliations.push({ - Tag: ExportNightwave.affiliationTag, - Standing: 0 - }) - 1 - ]; + if (affiliationMods.length == 0) { + affiliationMods.push({ Tag: nightwaveSyndicateTag }); + } + affiliationMods[0].Standing ??= 0; + affiliationMods[0].Standing += meta.standing!; } - affiliation.Standing += meta.standing; - - if (affiliationMods.length == 0) { - affiliationMods.push({ Tag: ExportNightwave.affiliationTag }); - } - affiliationMods[0].Standing ??= 0; - affiliationMods[0].Standing += meta.standing; } } return affiliationMods; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 32a2899c..908be883 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -79,6 +79,7 @@ import { config } from "./configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { ISyndicateMissionInfo } from "../types/worldStateTypes"; import { fromOid } from "../helpers/inventoryHelpers"; +import { TAccountDocument } from "./loginService"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { // For Spy missions, e.g. 3 vaults cracked = A, B, C @@ -135,6 +136,7 @@ const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | unde //const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys export const addMissionInventoryUpdates = async ( + account: TAccountDocument, inventory: TInventoryDatabaseDocument, inventoryUpdates: IMissionInventoryUpdateRequest ): Promise => { @@ -287,7 +289,7 @@ export const addMissionInventoryUpdates = async ( addRecipes(inventory, value); break; case "ChallengeProgress": - addChallenges(inventory, value, inventoryUpdates.SeasonChallengeCompletions); + addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions); break; case "FusionTreasures": addFusionTreasures(inventory, value); diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index f6917d7d..89154147 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -27,7 +27,6 @@ import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSal import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; -import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json"; @@ -54,7 +53,6 @@ const rawVendorManifests: IVendorManifest[] = [ Nova1999ConquestShopManifest, OstronPetVendorManifest, OstronProspectorVendorManifest, - RadioLegionIntermission12VendorManifest, SolarisDebtTokenVendorRepossessionsManifest, SolarisProspectorVendorManifest, Temple1999VendorManifest, diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index fa125b8a..7d91e027 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -6,7 +6,7 @@ import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { SRng } from "@/src/services/rngService"; -import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus"; +import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, ICalendarEvent, @@ -344,11 +344,26 @@ export const getSortie = (day: number): ISortie => { }; }; -const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => - x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") -); +interface IRotatingSeasonChallengePools { + daily: string[]; + weekly: string[]; + hardWeekly: string[]; +} -const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { +const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { + const syndicate = ExportSyndicates[syndicateTag]; + return { + daily: syndicate.dailyChallenges!, + weekly: syndicate.weeklyChallenges!.filter( + x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && + !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") + ), + hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")) + }; +}; + +const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => { const dayStart = EPOCH + day * 86400000; const dayEnd = EPOCH + (day + 3) * 86400000; const rng = new SRng(new SRng(day).randomInt(0, 100_000)); @@ -357,17 +372,11 @@ const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { Daily: true, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Challenge: rng.randomElement(dailyChallenges)! + Challenge: rng.randomElement(pools.daily)! }; }; -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 getSeasonWeeklyChallenge = (pools: IRotatingSeasonChallengePools, week: number, id: number): ISeasonChallenge => { const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; @@ -376,15 +385,15 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: rng.randomElement(weeklyChallenges)! + Challenge: rng.randomElement(pools.weekly)! }; }; -const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x => - x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") -); - -const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => { +const getSeasonWeeklyHardChallenge = ( + pools: IRotatingSeasonChallengePools, + week: number, + id: number +): ISeasonChallenge => { const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const challengeId = week * 7 + id; @@ -393,35 +402,39 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: rng.randomElement(weeklyHardChallenges)! + Challenge: rng.randomElement(pools.hardWeekly)! }; }; -const pushWeeklyActs = (worldState: IWorldState, week: number): void => { +const pushWeeklyActs = ( + activeChallenges: ISeasonChallenge[], + pools: IRotatingSeasonChallengePools, + week: number +): void => { const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); - worldState.SeasonInfo.ActiveChallenges.push({ + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0)); + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3)); + activeChallenges.push({ _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" }); - worldState.SeasonInfo.ActiveChallenges.push({ + activeChallenges.push({ _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" }); - worldState.SeasonInfo.ActiveChallenges.push({ + activeChallenges.push({ _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, Activation: { $date: { $numberLong: weekStart.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" }); }; @@ -926,15 +939,6 @@ export const getWorldState = (buildLabel?: string): IWorldState => { LiteSorties: [], GlobalUpgrades: [], EndlessXpChoices: [], - SeasonInfo: { - Activation: { $date: { $numberLong: "1715796000000" } }, - Expiry: { $date: { $numberLong: "2000000000000" } }, - AffiliationTag: "RadioLegionIntermission12Syndicate", - Season: 14, - Phase: 0, - Params: "", - ActiveChallenges: [] - }, KnownCalendarSeasons: [], ...staticWorldState, SyndicateMissions: [...staticWorldState.SyndicateMissions] @@ -967,17 +971,27 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } // Nightwave Challenges - // Current nightwave season was introduced in 38.0.8 so omitting challenges before that to avoid UI bugs and even crashes on really old versions. - if (!buildLabel || version_compare(buildLabel, "2025.02.05.11.19") >= 0) { - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0)); + const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); + if (nightwaveSyndicateTag) { + worldState.SeasonInfo = { + Activation: { $date: { $numberLong: "1715796000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + AffiliationTag: nightwaveSyndicateTag, + Season: nightwaveTagToSeason[nightwaveSyndicateTag], + Phase: 0, + Params: "", + ActiveChallenges: [] + }; + const pools = getSeasonChallengePools(nightwaveSyndicateTag); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0)); if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { - worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); } - pushWeeklyActs(worldState, week); + pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week); if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { - pushWeeklyActs(worldState, week + 1); + pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1); } } @@ -1242,3 +1256,18 @@ export const isArchwingMission = (node: IRegion): boolean => { } return false; }; + +export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => { + if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) { + return "RadioLegionIntermission13Syndicate"; + } + if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) { + return "RadioLegionIntermission12Syndicate"; + } + return undefined; +}; + +const nightwaveTagToSeason: Record = { + RadioLegionIntermission13Syndicate: 15, + RadioLegionIntermission12Syndicate: 14 +}; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 68c68753..78c1f330 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -14,7 +14,7 @@ export interface IWorldState { NodeOverrides: INodeOverride[]; PVPChallengeInstances: IPVPChallengeInstance[]; EndlessXpChoices: IEndlessXpChoice[]; - SeasonInfo: { + SeasonInfo?: { Activation: IMongoDate; Expiry: IMongoDate; AffiliationTag: string; diff --git a/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json b/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json deleted file mode 100644 index 33198a22..00000000 --- a/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json +++ /dev/null @@ -1,1192 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "67a04d500000000000000000" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Events/RadioLegionIntermission12VendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000049" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000050" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000051" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000052" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000053" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Alertium", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 5, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000054" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 10000, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000055" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Nightwave/GlassmakerShipDeco", - "ItemPrices": [ - { - "ItemCount": 60, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "Id": { - "$oid": "001500150000000000000056" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Hoverboard/HoverboardStickerWolf", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000057" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2MagAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000058" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessRhinoAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000059" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/NekrosShroudHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000060" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessAshAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000061" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/XakuAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000062" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessVaubanAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000063" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2TrinityAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000064" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/HildrynAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000065" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessFrostAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000066" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/Hood/RealOperatorWolfHoodBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000067" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2SarynAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000068" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/NidusAltTwoHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000069" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/OberonAltBHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000070" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StyanaxAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000071" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/ExcaliburMordredHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000072" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/RangerAltBHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000073" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2EmberAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000074" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/AnimaAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000075" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/JadeAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000076" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/NekrosAraknidHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000077" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/ProteaAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000078" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/PriestAlt2HelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000079" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/AtomosSolsticeSkinBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000080" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/DanteAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000081" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/RevenantAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000082" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/SandmanAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000083" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerLootRadarAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000084" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerRifleDamageAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000085" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerShellAmmoAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000086" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Nightwave/BroncoNightwaveMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000087" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000088" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Nightwave/MagnusNightwaveMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000089" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Event/Nightwave/NightwaveBattacorAugmentMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000090" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/GrnAxeBlueprint", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000091" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/DesertGrinlokSkinBlueprint", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000092" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/HeatDaggerBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000093" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/GlaiveBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000094" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/DarkDaggerBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000095" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Sigils/NoraSeasonTwoSigil", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000096" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/DarkSwordBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000097" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/PlasmaSwordBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000098" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/ShockExergisSkinBlueprint", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000099" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumperMediumDirty", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000100" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/PvPMods/Rifle/TetraFasterProjAiming", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000101" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorPvPAugmentCard", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000102" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/PvPMods/Rifle/SupraHigherAccuracyAiming", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000103" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/PvPMods/Rifle/RubicoLowZoom", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000104" - } - } - ], - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - } - } -} From 082ae536f7c9386090c17d6a80b7128e89ff561d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 24 May 2025 01:48:10 -0700 Subject: [PATCH 05/30] fix: more robust address detection (#2099) Previously this had a few issues when using non-standard ports for HTTP(S) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2099 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 | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 2007db12..3d510f26 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -20,7 +20,21 @@ 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; + let myAddress: string; + let myUrlBase: string = request.protocol + "://"; + if (request.host.indexOf("warframe.com") == -1) { + // Client request was redirected cleanly, so we know it can reach us how it's reaching us now. + myAddress = request.hostname; + myUrlBase += request.host; + } else { + // Don't know how the client reached us, hoping the config does. + myAddress = config.myAddress; + myUrlBase += myAddress; + const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443; + if (port != (request.protocol == "http" ? 80 : 443)) { + myUrlBase += ":" + port; + } + } if ( !account && @@ -52,7 +66,7 @@ export const loginController: RequestHandler = async (request, response) => { LastLogin: new Date() }); logger.debug("created new account"); - response.json(createLoginResponse(myAddress, newAccount, buildLabel)); + response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel)); return; } catch (error: unknown) { if (error instanceof Error) { @@ -98,10 +112,15 @@ export const loginController: RequestHandler = async (request, response) => { } await account.save(); - response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel)); + response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); }; -const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { +const createLoginResponse = ( + myAddress: string, + myUrlBase: string, + account: IDatabaseAccountJson, + buildLabel: string +): ILoginResponse => { const resp: ILoginResponse = { id: account.id, DisplayName: account.DisplayName, @@ -139,11 +158,11 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b } if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { resp.CrossPlatformAllowed = account.CrossPlatformAllowed; - resp.HUB = `https://${myAddress}/api/`; + resp.HUB = `${myUrlBase}/api/`; resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; } if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { - resp.platformCDNs = [`https://${myAddress}/`]; + resp.platformCDNs = [`${myUrlBase}/`]; } return resp; }; From d41e4f7f5694bb31807e38a09f1ec15569caa389 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 24 May 2025 01:48:25 -0700 Subject: [PATCH 06/30] chore: restart web server when ports in config have changed (#2100) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2100 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/index.ts | 27 ++---------- src/services/configWatcherService.ts | 7 +++ src/services/webService.ts | 65 ++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 24 deletions(-) create mode 100644 src/services/webService.ts diff --git a/src/index.ts b/src/index.ts index f688b9e1..80adc252 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,12 +12,10 @@ 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 mongoose from "mongoose"; import { JSONStringify } from "json-with-bigint"; +import { startWebServer } from "./services/webService"; + import { validateConfig } from "@/src/services/configWatcherService"; // Patch JSON.stringify to work flawlessly with Bigints. @@ -29,26 +27,7 @@ mongoose .connect(config.mongodbUrl) .then(() => { logger.info("Connected to MongoDB"); - - const httpPort = config.httpPort || 80; - const httpsPort = config.httpsPort || 443; - const options = { - key: fs.readFileSync("static/certs/key.pem"), - cert: fs.readFileSync("static/certs/cert.pem") - }; - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - http.createServer(app).listen(httpPort, () => { - logger.info("HTTP server started on port " + httpPort); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - https.createServer(options, app).listen(httpsPort, () => { - logger.info("HTTPS server started on port " + httpsPort); - - logger.info( - "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort) - ); - }); - }); + startWebServer(); }) .catch(error => { if (error instanceof Error) { diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts index e8584785..46c236ea 100644 --- a/src/services/configWatcherService.ts +++ b/src/services/configWatcherService.ts @@ -2,6 +2,7 @@ import fs from "fs"; import fsPromises from "fs/promises"; import { logger } from "../utils/logger"; import { config, configPath, loadConfig } from "./configService"; +import { getWebPorts, startWebServer, stopWebServer } from "./webService"; let amnesia = false; fs.watchFile(configPath, () => { @@ -16,6 +17,12 @@ fs.watchFile(configPath, () => { process.exit(1); } validateConfig(); + + const webPorts = getWebPorts(); + if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) { + logger.info(`Restarting web server to apply port changes.`); + void stopWebServer().then(startWebServer); + } } }); diff --git a/src/services/webService.ts b/src/services/webService.ts new file mode 100644 index 00000000..77fe01fe --- /dev/null +++ b/src/services/webService.ts @@ -0,0 +1,65 @@ +import http from "http"; +import https from "https"; +import fs from "node:fs"; +import { config } from "./configService"; +import { logger } from "../utils/logger"; +import { app } from "../app"; +import { AddressInfo } from "node:net"; + +let httpServer: http.Server | undefined; +let httpsServer: https.Server | undefined; + +const tlsOptions = { + key: fs.readFileSync("static/certs/key.pem"), + cert: fs.readFileSync("static/certs/cert.pem") +}; + +export const startWebServer = (): void => { + const httpPort = config.httpPort || 80; + const httpsPort = config.httpsPort || 443; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + httpServer = http.createServer(app); + httpServer.listen(httpPort, () => { + logger.info("HTTP server started on port " + httpPort); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + httpsServer = https.createServer(tlsOptions, app); + httpsServer.listen(httpsPort, () => { + logger.info("HTTPS server started on port " + httpsPort); + + logger.info( + "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort) + ); + }); + }); +}; + +export const getWebPorts = (): Record<"http" | "https", number | undefined> => { + return { + http: (httpServer?.address() as AddressInfo | undefined)?.port, + https: (httpsServer?.address() as AddressInfo | undefined)?.port + }; +}; + +export const stopWebServer = async (): Promise => { + const promises: Promise[] = []; + if (httpServer) { + promises.push( + new Promise(resolve => { + httpServer!.close(() => { + resolve(); + }); + }) + ); + } + if (httpsServer) { + promises.push( + new Promise(resolve => { + httpsServer!.close(() => { + resolve(); + }); + }) + ); + } + await Promise.all(promises); +}; From 84916bf64e7a4e628d7ab1d96fc70a2063749780 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 24 May 2025 01:48:41 -0700 Subject: [PATCH 07/30] fix: resolve random relic booster pack for login reward (#2101) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2101 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/loginRewardService.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts index 1f94807b..c8a213ea 100644 --- a/src/services/loginRewardService.ts +++ b/src/services/loginRewardService.ts @@ -5,7 +5,13 @@ import { mixSeeds, SRng } 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 { + ExportBoosterPacks, + ExportBoosters, + ExportRecipes, + ExportWarframes, + ExportWeapons +} from "warframe-public-export-plus"; import { toStoreItem } from "./itemDataService"; export interface ILoginRewardsReponse { @@ -76,6 +82,7 @@ export const getRandomLoginRewards = ( const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { const reward = rng.randomReward(randomRewards)!; //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; + let storeItemType: string = reward.StoreItemType; if (reward.RewardType == "RT_RANDOM_RECIPE") { const masteredItems = new Set(); for (const entry of inventory.XPInfo) { @@ -102,7 +109,12 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward. return getRandomLoginReward(rng, day, inventory); } - reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!); + storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!); + } else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") { + storeItemType = toStoreItem( + rng.randomElement(ExportBoosterPacks["/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"].components)! + .Item + ); } return { //_id: toOid(new Types.ObjectId()), @@ -110,7 +122,7 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab //CouponType: "CPT_PLATINUM", Icon: reward.Icon ?? "", //ItemType: "", - StoreItemType: reward.StoreItemType, + StoreItemType: storeItemType, //ProductCategory: "Pistols", Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)), ScalingMultiplier: reward.ScalingMultiplier, From 90f2b90398dfedc5cd986dcffb2beaff358c5224 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 24 May 2025 01:48:50 -0700 Subject: [PATCH 08/30] chore: switch to official tsgo preview package (#2103) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2103 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-lock.json | 173 +++++++++++++++++++++++++++++------- package.json | 6 +- 3 files changed, 145 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a59167d1..aed7014e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,8 @@ jobs: uses: actions/checkout@v4.1.2 - name: Setup Node.js environment uses: actions/setup-node@v4.0.2 + with: + node-version: ">=20.6.0" - run: npm ci - run: cp config.json.example config.json - run: npm run verify diff --git a/package-lock.json b/package-lock.json index c1dea433..59407f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,18 +24,14 @@ "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", + "@typescript/native-preview": "^7.0.0-dev.20250523.1", "eslint": "^8", "eslint-plugin-prettier": "^5.2.5", "prettier": "^3.5.3", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0" - }, - "engines": { - "node": ">=18.15.0", - "npm": ">=9.5.0" } }, "node_modules/@colors/colors": { @@ -308,32 +304,6 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@rxliuli/tsgo": { - "version": "2025.5.8", - "resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.5.8.tgz", - "integrity": "sha512-P3/qxcUgiWz6nSJslJ5mMeAEqacK8LQSoOhdvHxI1/d0Xqxt2Qp6/nmhWuOlyqnCyAaIoXgoiUshiXWBGr2jaw==", - "cpu": [ - "x64", - "ia32", - "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", @@ -695,6 +665,147 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@typescript/native-preview": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-CgdgP/gmyaMThY7Fho19nDaTVryn9QV/zD/6w1KfDCn3M4Rq4WvkSc7Ob1ohc4V1XjCSIzg6Ul+HbLEc7xvV4Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsgo": "bin/tsgo.js" + }, + "engines": { + "node": ">=20.6.0" + }, + "optionalDependencies": { + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20250523.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20250523.1" + } + }, + "node_modules/@typescript/native-preview-darwin-arm64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-oWJMPD+lfH9/dvHhPSZdTv43lfyZGrn7crytefhkiQPSwP0MIUCpnDkofGP/ML1nv0xx0pwWhH+Ein88NW3LuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-darwin-x64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-Yk8bJEsYsRKgRqYlwPvh7DPdgBMC/oPN60X0LWeuMLci65+4kyqF8Cv6K/W3ABc005cB4tYn4iR+9T6zipvrKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-linux-arm": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-B+8CRIv6ebL8gzAagnJP8wml3baFV2FtFWuXYl6jlAcLGoQOh/yGdcAueZoJjJKNod4gAOl8OJoTicuC0BVIxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-linux-arm64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-IErNI08z9qE6mHaJaT6tM7il8j21ryH3DNVyFP4yz5FTKnkXFj1Kb4NcI41Q8w226LTQgBR8kNErVlbUWr7ywA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-linux-x64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-TCZtknsLUgPRaEfX9CvBZNgrHhMRZPYYZgF1Aasdv0PONv9mB8w0Xforgxoo4UFjdF5ZzOu2icgc7sKJJeu5vw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-win32-arm64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-bulwrkLEkoY4Jqeuvfz24RiVOiZZ7Rr9TblFqZAgZFZOnyXuhjM1jE8F1hnJFC5AghJe2HdLD3EKfabqlffrIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-win32-x64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-ztzfO0oF/rj8xO5y3SyAcigmgvgczrqobCugEWFqiYumteWZPN2MYWcNYk2k8Y5LAgg1fN1xHIg8RRSPoo6XUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.6.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", diff --git a/package.json b/package.json index 272736d2..3eaff1b9 100644 --- a/package.json +++ b/package.json @@ -31,17 +31,13 @@ "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", + "@typescript/native-preview": "^7.0.0-dev.20250523.1", "eslint": "^8", "eslint-plugin-prettier": "^5.2.5", "prettier": "^3.5.3", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0" - }, - "engines": { - "node": ">=18.15.0", - "npm": ">=9.5.0" } } From b90bdd2783b7c6f6c581850684e2588c46eb268e Mon Sep 17 00:00:00 2001 From: Vitruvio Date: Mon, 26 May 2025 01:46:19 -0700 Subject: [PATCH 09/30] chore: update french translation (#2104) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2104 Co-authored-by: Vitruvio Co-committed-by: Vitruvio --- static/webui/translations/fr.js | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 6c91356c..5a61111e 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -34,8 +34,8 @@ dict = { code_rerollsNumber: `Nombre de rerolls`, code_viewStats: `Voir les stats`, code_rank: `Rang`, - code_rankUp: `[UNTRANSLATED] Rank up`, - code_rankDown: `[UNTRANSLATED] Rank down`, + code_rankUp: `Monter de rang`, + code_rankDown: `Baisser de 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.`, @@ -59,7 +59,7 @@ dict = { login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, login_loginButton: `Connexion`, - login_registerButton: `[UNTRANSLATED] Register`, + login_registerButton: `S'enregistrer`, navbar_logout: `Déconnexion`, navbar_renameAccount: `Renommer le compte`, navbar_deleteAccount: `Supprimer le compte`, @@ -83,21 +83,21 @@ dict = { inventory_hoverboards: `K-Drives`, inventory_moaPets: `Moas`, inventory_kubrowPets: `Bêtes`, - inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, + inventory_evolutionProgress: `Progrès de l'évolution Incarnon`, inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, inventory_bulkAddWeapons: `Ajouter les armes manquantes`, inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`, inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`, inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`, - inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, - inventory_bulkRankUpSuits: `Toutes les Warframes rang max`, - inventory_bulkRankUpWeapons: `Toutes les armes rang max`, - inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`, - inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`, - inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`, - inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`, - inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, + inventory_bulkAddEvolutionProgress: `Ajouter les évolutions Incarnon manquantes`, + inventory_bulkRankUpSuits: `Toutes les Warframes au rang max`, + inventory_bulkRankUpWeapons: `Toutes les armes au rang max`, + inventory_bulkRankUpSpaceSuits: `Tous les Archwings au rang max`, + inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing au rang max`, + inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`, + inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`, + inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`, quests_list: `Quêtes`, quests_completeAll: `Compléter toutes les quêtes`, @@ -117,9 +117,9 @@ dict = { mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, mods_rivens: `Rivens`, mods_mods: `Mods`, - mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, - mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, - mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, + mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`, + mods_removeUnranked: `Retirer les mods sans rang`, + mods_addMissingMaxRankMods: `Ajouter les mods niveau max 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`, @@ -131,9 +131,9 @@ dict = { cheats_infiniteEndo: `Endo infini`, cheats_infiniteRegalAya: `Aya Raffiné infini`, cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`, - cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, - cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`, - cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, + cheats_claimingBlueprintRefundsIngredients: `Récupérer les items rend les ressources`, + cheats_dontSubtractVoidTraces: `Ne pas consommer de Void Traces`, + cheats_dontSubtractConsumables: `Ne pas retirer de consommables`, 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`, @@ -151,11 +151,11 @@ dict = { cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`, cheats_noDeathMarks: `Aucune marque d'assassin`, cheats_noKimCooldowns: `Aucun cooldown sur le KIM`, - cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`, - cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`, + cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`, + cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`, cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`, cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`, - cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, + cheats_skipClanKeyCrafting: `Passer le craft de la clé de clan`, cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`, cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`, cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`, From ae1850d6cd450f46f7f28b7f2d41392797ad0555 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 27 May 2025 03:58:12 -0700 Subject: [PATCH 10/30] chore: update PE+ (#2105) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2105 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/serversideVendorsService.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59407f15..c1730599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.64", + "warframe-public-export-plus": "^0.5.65", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3814,9 +3814,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.64", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.64.tgz", - "integrity": "sha512-JyHRtYumfwQ1Iog2unzlBWfQHJlZER+iUISquyFFv0Qqtv2QsNzFv2AbV7sCaqgDcE8tw6e5/YqGgfI0m403/g==" + "version": "0.5.65", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.65.tgz", + "integrity": "sha512-y/HN61lE5g8gx0Giutdl/jzQnQmw1u2uI0BiwKVW341nf42sKWQPsKsCVTL5x9MIDYyRCbFsMU+PazKC7byMdg==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 3eaff1b9..d540ff13 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.64", + "warframe-public-export-plus": "^0.5.65", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 89154147..0282c562 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -303,7 +303,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani } const cycleStart = cycleOffset + cycleIndex * cycleDuration; for (const rawItem of offersToAdd) { - const durationHoursRange = toRange(rawItem.durationHours); + const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration); const expiry = cycleStart + rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; From 28da982c8049c192148aa9475274d692c0d0efa5 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 30 May 2025 11:42:45 -0700 Subject: [PATCH 11/30] feat: renamePet (#2106) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2106 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/renamePetController.ts | 23 ++++++++++++++++++++++ src/routes/api.ts | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 src/controllers/api/renamePetController.ts diff --git a/src/controllers/api/renamePetController.ts b/src/controllers/api/renamePetController.ts new file mode 100644 index 00000000..6672d064 --- /dev/null +++ b/src/controllers/api/renamePetController.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 renamePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree"); + const data = getJSONfromString(String(req.body)); + const details = inventory.KubrowPets.id(data.petId)!.Details!; + details.Name = data.name; + const currencyChanges = updateCurrency(inventory, 15, true); + await inventory.save(); + res.json({ + ...data, + inventoryChanges: currencyChanges + }); +}; + +interface IRenamePetRequest { + petId: string; + name: string; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 82b42842..7db66dfc 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -107,6 +107,7 @@ import { removeFriendGetController, removeFriendPostController } from "@/src/con import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; +import { renamePetController } from "@/src/controllers/api/renamePetController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; @@ -294,6 +295,7 @@ apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/removeFriend.php", removeFriendPostController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); +apiRouter.post("/renamePet.php", renamePetController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); From 32f4c5105ab20a4a6aadfdb10107e0c1d7bb491e Mon Sep 17 00:00:00 2001 From: Sainan Date: Sun, 1 Jun 2025 03:12:07 -0700 Subject: [PATCH 12/30] feat: add update and start script for linux (#2107) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2107 Co-authored-by: Sainan Co-committed-by: Sainan --- UPDATE AND START SERVER.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 UPDATE AND START SERVER.sh diff --git a/UPDATE AND START SERVER.sh b/UPDATE AND START SERVER.sh new file mode 100755 index 00000000..d33afba9 --- /dev/null +++ b/UPDATE AND START SERVER.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +echo "Updating SpaceNinjaServer..." +git fetch --prune +git stash +git reset --hard origin/main + +if [ -d "static/data/0/" ]; then + echo "Updating stripped assets..." + cd static/data/0/ + git pull + cd ../../../ +fi + +echo "Updating dependencies..." +npm i --omit=dev + +npm run build +if [ $? -eq 0 ]; then + npm run start + echo "SpaceNinjaServer seems to have crashed." +fi + From 8f02bd150934e324855368001e1f542a8fc16409 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 1 Jun 2025 03:12:14 -0700 Subject: [PATCH 13/30] fix: avoid addition by undefined when adding skill points (#2108) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2108 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 4 ++-- src/types/requestTypes.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 908be883..7d8675c5 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -326,8 +326,8 @@ export const addMissionInventoryUpdates = async ( break; } case "PlayerSkillGains": { - inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE; - inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER; + inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0; + inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0; break; } case "CustomMarkers": { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 6fdd0c09..a6b3f1c1 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -96,7 +96,7 @@ export type IMissionInventoryUpdateRequest = { FpsSamples: number; EvolutionProgress?: IEvolutionProgress[]; FocusXpIncreases?: number[]; - PlayerSkillGains: IPlayerSkills; + PlayerSkillGains: Partial; CustomMarkers?: ICustomMarkers[]; LoreFragmentScans?: ILoreFragmentScan[]; VoidTearParticipantsCurrWave?: { From b0499a62aae7fa5156dc2e677abac9798db177c7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 1 Jun 2025 12:16:16 +0200 Subject: [PATCH 14/30] chore: use 'git checkout -f' instead of 'git reset --hard' to avoid loss --- UPDATE AND START SERVER.bat | 2 +- UPDATE AND START SERVER.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index 983b8f8d..7f0bd170 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -3,7 +3,7 @@ echo Updating SpaceNinjaServer... git fetch --prune git stash -git reset --hard origin/main +git checkout -f origin/main if exist static\data\0\ ( echo Updating stripped assets... diff --git a/UPDATE AND START SERVER.sh b/UPDATE AND START SERVER.sh index d33afba9..e57c8061 100755 --- a/UPDATE AND START SERVER.sh +++ b/UPDATE AND START SERVER.sh @@ -3,7 +3,7 @@ echo "Updating SpaceNinjaServer..." git fetch --prune git stash -git reset --hard origin/main +git checkout -f origin/main if [ -d "static/data/0/" ]; then echo "Updating stripped assets..." From d43e39d7b5ecb99b54de07357cbf03111a10d05d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 2 Jun 2025 01:44:10 -0700 Subject: [PATCH 15/30] fix(webui): error when an unknown suit is max rank (#2109) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2109 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index d8d30a41..718dc840 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -476,7 +476,7 @@ function updateInventory() { } let anyExaltedMissingXP = false; - if (item.XP >= maxXP && "exalted" in itemMap[item.ItemType]) { + if (item.XP >= maxXP && item.ItemType in itemMap && "exalted" in itemMap[item.ItemType]) { for (const exaltedType of itemMap[item.ItemType].exalted) { const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType); if (exaltedItem) { From d739945a1d81826d1134175ab6307ad0ac9ce925 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:40:28 -0700 Subject: [PATCH 16/30] fix: check that syndicateMissionId is not undefined (#2110) Closes #2111 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2110 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, 6 insertions(+), 2 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7d8675c5..9c7dc93a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1207,7 +1207,9 @@ export const addMissionRewards = async ( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_"); const syndicateMissions: ISyndicateMissionInfo[] = []; - pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + if (syndicateMissionId) { + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + } const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry && syndicateEntry.Jobs) { let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; @@ -1556,7 +1558,9 @@ function getRandomMissionDrops( let isEndlessJob = false; if (syndicateMissionId) { const syndicateMissions: ISyndicateMissionInfo[] = []; - pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + if (syndicateMissionId) { + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + } const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry && syndicateEntry.Jobs) { let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; From 01492f4f16980f174eea865078930164843dc414 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:59:22 -0700 Subject: [PATCH 17/30] fix: swapped operands (#2115) Closes #2113 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2115 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 9c305103..abd9180e 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -174,7 +174,7 @@ export const nemesisController: RequestHandler = async (req, res) => { body.target.fp = BigInt(body.target.fp); const manifest = getNemesisManifest(body.target.manifest); - if (account.BuildLabel && version_compare(manifest.minBuild, account.BuildLabel) < 0) { + if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) { logger.warn( `client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.` ); From 20c4092dfeff04fdc93db7435fecbf862f0b9d26 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:13:36 -0700 Subject: [PATCH 18/30] fix: swapped operands (#2119) Closes #2118 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2119 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/inventoryController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 97da2c65..df04e9d4 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -310,7 +310,7 @@ export const getInventoryResponse = async ( // Fix nemesis for older versions if ( inventoryResponse.Nemesis && - version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0 + version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0 ) { inventoryResponse.Nemesis = undefined; } From be02435661b402e1f9196efc388b2af9d57453a7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:29:25 -0700 Subject: [PATCH 19/30] chore: handle addItem of /Lotus/Types/Items/Emotes/** based on path (#2116) Closes #2114 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2116 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 4ccd9c5c..f972a608 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -791,6 +791,12 @@ export const addItem = async ( } break; } + case "Items": { + if (typeName.substr(1).split("/")[3] == "Emotes") { + return addCustomization(inventory, typeName); + } + break; + } case "NeutralCreatures": { if (inventory.Horses.length != 0) { logger.warn("refusing to add Horse because account already has one"); From 0997f9567f0843fc274564b9cde83d9bdff4515d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:59:13 -0700 Subject: [PATCH 20/30] fix: cap nemesis rank (#2122) Re #2121 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2122 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 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index abd9180e..435f6791 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -150,17 +150,17 @@ export const nemesisController: RequestHandler = async (req, res) => { res.json(response); } else { const passcode = getNemesisPasscode(inventory.Nemesis!); + let RankIncrease: number | undefined; if (passcode[body.position] != body.guess) { - res.end(); - } else { - inventory.Nemesis!.Rank += 1; - inventory.Nemesis!.InfNodes = getInfNodes( - getNemesisManifest(inventory.Nemesis!.manifest), - inventory.Nemesis!.Rank - ); + const manifest = getNemesisManifest(inventory.Nemesis!.manifest); + if (inventory.Nemesis!.Rank + 1 < manifest.systemIndexes.length) { + inventory.Nemesis!.Rank += 1; + RankIncrease = 1; + } + inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); await inventory.save(); - res.json({ RankIncrease: 1 }); } + res.json({ RankIncrease }); } } else if ((req.query.mode as string) == "rs") { // report spawn; POST but no application data in body From 8f5f2fc206471f1188917f21934d8dfc27ec83f7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:15:35 -0700 Subject: [PATCH 21/30] chore: handle numbers in config administratorNames (#2117) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2117 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/configWatcherService.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts index 46c236ea..77c56d9a 100644 --- a/src/services/configWatcherService.ts +++ b/src/services/configWatcherService.ts @@ -27,9 +27,21 @@ fs.watchFile(configPath, () => { }); export const validateConfig = (): void => { - if (typeof config.administratorNames == "string") { - logger.info(`Updating config.json to make administratorNames an array.`); - config.administratorNames = [config.administratorNames]; + let modified = false; + if (config.administratorNames) { + if (!Array.isArray(config.administratorNames)) { + config.administratorNames = [config.administratorNames]; + modified = true; + } + for (let i = 0; i != config.administratorNames.length; ++i) { + if (typeof config.administratorNames[i] != "string") { + config.administratorNames[i] = String(config.administratorNames[i]); + modified = true; + } + } + } + if (modified) { + logger.info(`Updating config.json to fix some issues with it.`); void saveConfig(); } }; From 5c5296d56549db07a64d1a6c60e91d4e387dace6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:16:19 -0700 Subject: [PATCH 22/30] feat: add nightwaveOverride to config (#2120) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2120 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- README.md | 17 +++++++ config.json.example | 3 +- src/services/configService.ts | 1 + src/services/worldStateService.ts | 77 ++++++++++++++++++++++--------- 4 files changed, 74 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e1ef957a..24d3b2fb 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,20 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi - `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. +- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values: + - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9 + - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8 + - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7 + - `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6 + - `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5 + - `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4 + - `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3 + - `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2 + - `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1 + - `RadioLegionIntermission4Syndicate` for Nora's Choice + - `RadioLegionIntermission3Syndicate` for Intermission III + - `RadioLegion3Syndicate` for Glassmaker + - `RadioLegionIntermission2Syndicate` for Intermission II + - `RadioLegion2Syndicate` for The Emissary + - `RadioLegionIntermissionSyndicate` for Intermission I + - `RadioLegionSyndicate` for The Wolf of Saturn Six diff --git a/config.json.example b/config.json.example index d9585902..4000024f 100644 --- a/config.json.example +++ b/config.json.example @@ -55,6 +55,7 @@ "affinityBoost": false, "resourceBoost": false, "starDays": true, - "lockTime": 0 + "lockTime": 0, + "nightwaveOverride": "" } } diff --git a/src/services/configService.ts b/src/services/configService.ts index 6a42481b..ba67cf06 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -62,6 +62,7 @@ interface IConfig { resourceBoost?: boolean; starDays?: boolean; lockTime?: number; + nightwaveOverride?: string; }; } diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 7d91e027..9c887445 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -348,6 +348,7 @@ interface IRotatingSeasonChallengePools { daily: string[]; weekly: string[]; hardWeekly: string[]; + hasWeeklyPermanent: boolean; } const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { @@ -359,7 +360,12 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") ), - hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")) + hardWeekly: syndicate.weeklyChallenges!.filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") + ), + hasWeeklyPermanent: !!syndicate.weeklyChallenges!.find(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") + ) }; }; @@ -416,26 +422,34 @@ const pushWeeklyActs = ( activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0)); activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1)); - activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2)); - activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3)); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" - }); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" - }); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" - }); + if (pools.hasWeeklyPermanent) { + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + }); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + }); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + }); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3)); + } else { + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 2)); + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 3)); + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 4)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 5)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 6)); + } }; export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => { @@ -1258,6 +1272,9 @@ export const isArchwingMission = (node: IRegion): boolean => { }; export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => { + if (config.worldState?.nightwaveOverride) { + return config.worldState.nightwaveOverride; + } if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) { return "RadioLegionIntermission13Syndicate"; } @@ -1268,6 +1285,20 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string }; const nightwaveTagToSeason: Record = { - RadioLegionIntermission13Syndicate: 15, - RadioLegionIntermission12Syndicate: 14 + RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9 + RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8 + RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7 + RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6 + RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5 + RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4 + RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3 + RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2 + RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1 + RadioLegionIntermission4Syndicate: 6, // Nora's Choice + RadioLegionIntermission3Syndicate: 5, // Intermission III + RadioLegion3Syndicate: 4, // Glassmaker + RadioLegionIntermission2Syndicate: 3, // Intermission II + RadioLegion2Syndicate: 2, // The Emissary + RadioLegionIntermissionSyndicate: 1, // Intermission I + RadioLegionSyndicate: 0 // The Wolf of Saturn Six }; From 2e649cabf6e7e8b6e5d0c146ba598db11b7648f7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:16:33 -0700 Subject: [PATCH 23/30] chore: handle purchasing decree from acrithis (#2124) Closes #2112 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2124 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index f972a608..28832737 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -722,6 +722,10 @@ export const addItem = async ( } break; + case "Boons": + // Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory. + return {}; + case "Stickers": { const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName); From 8ffbb308c568df093b407678219026b09bd19614 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:16:50 -0700 Subject: [PATCH 24/30] fix: oull being considered an incorrect guess (#2125) Closes #2121 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2125 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 | 21 ++++++++++++--------- src/helpers/nemesisHelpers.ts | 3 +++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 435f6791..4a206d00 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -7,6 +7,7 @@ import { getNemesisManifest, getNemesisPasscode, getNemesisPasscodeModTypes, + GUESS_WILDCARD, IKnifeResponse } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; @@ -82,7 +83,7 @@ export const nemesisController: RequestHandler = async (req, res) => { } } else { for (let i = 0; i != 3; ++i) { - if (body.guess[i] == passcode[i]) { + if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) { ++guessResult; } } @@ -149,16 +150,18 @@ export const nemesisController: RequestHandler = async (req, res) => { await inventory.save(); res.json(response); } else { - const passcode = getNemesisPasscode(inventory.Nemesis!); let RankIncrease: number | undefined; - if (passcode[body.position] != body.guess) { - const manifest = getNemesisManifest(inventory.Nemesis!.manifest); - if (inventory.Nemesis!.Rank + 1 < manifest.systemIndexes.length) { - inventory.Nemesis!.Rank += 1; - RankIncrease = 1; + if (body.guess != GUESS_WILDCARD) { + const passcode = getNemesisPasscode(inventory.Nemesis!); + if (passcode[body.position] != body.guess) { + const manifest = getNemesisManifest(inventory.Nemesis!.manifest); + if (inventory.Nemesis!.Rank + 1 < manifest.systemIndexes.length) { + inventory.Nemesis!.Rank += 1; + RankIncrease = 1; + } + inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); + await inventory.save(); } - inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); - await inventory.save(); } res.json({ RankIncrease }); } diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 9d6a7a2b..0c2eebbe 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -266,6 +266,9 @@ export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNeme : passcode.map(i => reqiuemMods[i]); }; +export const GUESS_NONE = 8; +export const GUESS_WILDCARD = 9; + export const encodeNemesisGuess = ( symbol1: number, result1: number, From 4118528603474370253c73b354cf6bac518ae670 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:17:05 -0700 Subject: [PATCH 25/30] chore: some minor improvements to nemesis mode=s (#2126) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2126 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 | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 4a206d00..1efd0023 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -173,6 +173,9 @@ export const nemesisController: RequestHandler = async (req, res) => { res.json({ LastEnc: inventory.Nemesis!.LastEnc }); } else if ((req.query.mode as string) == "s") { const inventory = await getInventory(account._id.toString(), "Nemesis"); + if (inventory.Nemesis) { + logger.warn(`overwriting an existing nemesis as a new one is being requested`); + } const body = getJSONfromString(String(req.body)); body.target.fp = BigInt(body.target.fp); @@ -188,13 +191,15 @@ export const nemesisController: RequestHandler = async (req, res) => { const weapons: readonly string[] = manifest.weapons; const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); weaponIdx = initialWeaponIdx; - do { - const weapon = weapons[weaponIdx]; - if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { - break; - } - weaponIdx = (weaponIdx + 1) % weapons.length; - } while (weaponIdx != initialWeaponIdx); + if (body.target.DisallowedWeapons) { + do { + const weapon = weapons[weaponIdx]; + if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + } } inventory.Nemesis = { @@ -215,10 +220,10 @@ export const nemesisController: RequestHandler = async (req, res) => { GuessHistory: [], Hints: [], HintProgress: 0, - Weakened: body.target.Weakened, + Weakened: false, PrevOwners: 0, HenchmenKilled: 0, - SecondInCommand: body.target.SecondInCommand, + SecondInCommand: false, MissionCount: 0, LastEnc: 0 }; @@ -279,7 +284,7 @@ interface INemesisStartRequest { KillingSuit: string; killingDamageType: number; ShoulderHelmet: string; - DisallowedWeapons: string[]; + DisallowedWeapons?: string[]; WeaponIdx: number; AgentIdx: number; BirthNode: string; From 65387ccdea36c6c235fa5f7fd01a860fd58853d3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:17:27 -0700 Subject: [PATCH 26/30] feat(webui): disambiguate gear and resource with the same name for add items (#2127) Closes #2097 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2127 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../custom/getItemListsController.ts | 7 +++-- static/webui/script.js | 28 +++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 716104a5..4f03ee94 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -25,6 +25,7 @@ import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; interface ListedItem { uniqueName: string; name: string; + subtype?: string; fusionLimit?: number; exalted?: string[]; badReason?: "starter" | "frivolous" | "notraw"; @@ -175,7 +176,8 @@ const getItemListsController: RequestHandler = (req, response) => { ) { res.miscitems.push({ uniqueName: uniqueName, - name: name + name: name, + subtype: "Resource" }); } } @@ -193,7 +195,8 @@ const getItemListsController: RequestHandler = (req, response) => { for (const [uniqueName, item] of Object.entries(ExportGear)) { res.miscitems.push({ uniqueName: uniqueName, - name: getString(item.name, lang) + name: getString(item.name, lang), + subtype: "Gear" }); } const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang); diff --git a/static/webui/script.js b/static/webui/script.js index 718dc840..f4abcb9f 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -312,7 +312,7 @@ function fetchItemList() { document.getElementById("changeSyndicate").appendChild(option); }); } else { - const nameSet = new Set(); + const nameToItems = {}; items.forEach(item => { item.name = item.name.replace(/<.+>/g, "").trim(); if ("badReason" in item) { @@ -322,6 +322,11 @@ function fetchItemList() { item.name += " " + loc("code_badItem"); } } + nameToItems[item.name] ??= []; + nameToItems[item.name].push(item); + }); + + items.forEach(item => { if (type == "ModularParts") { const supportedModularParts = [ "LWPT_HB_DECK", @@ -360,15 +365,26 @@ function fetchItemList() { .appendChild(option); } } else if (item.badReason != "notraw") { - if (nameSet.has(item.name)) { - //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`); - } else { - nameSet.add(item.name); - + const ambiguous = nameToItems[item.name].length > 1; + let canDisambiguate = true; + if (ambiguous) { + for (const i2 of nameToItems[item.name]) { + if (!i2.subtype) { + canDisambiguate = false; + break; + } + } + } + if (!ambiguous || canDisambiguate || nameToItems[item.name][0] == item) { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; + if (ambiguous && canDisambiguate) { + option.value += " (" + item.subtype + ")"; + } document.getElementById("datalist-" + type).appendChild(option); + } else { + //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`); } } itemMap[item.uniqueName] = { ...item, type }; From 9def5c265e4ed1ddf356e512eb597b9a0c515381 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:45:50 -0700 Subject: [PATCH 27/30] feat: kubrow & kavat incubation (#2131) Closes #377 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2131 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/adoptPetController.ts | 27 ++++++++++++ .../api/claimCompletedRecipeController.ts | 23 ++++++++-- src/controllers/api/startRecipeController.ts | 43 ++++++++++++------- src/models/inventoryModels/inventoryModel.ts | 4 +- src/routes/api.ts | 2 + src/services/inventoryService.ts | 18 +++++--- src/types/inventoryTypes/inventoryTypes.ts | 9 +++- 7 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 src/controllers/api/adoptPetController.ts diff --git a/src/controllers/api/adoptPetController.ts b/src/controllers/api/adoptPetController.ts new file mode 100644 index 00000000..3cd340da --- /dev/null +++ b/src/controllers/api/adoptPetController.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 adoptPetController: 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.Name = data.name; + await inventory.save(); + res.json({ + petId: data.petId, + newName: data.name + } satisfies IAdoptPetResponse); +}; + +interface IAdoptPetRequest { + petId: string; + name: string; +} + +interface IAdoptPetResponse { + petId: string; + newName: string; +} diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index e519d170..6482c9a3 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -17,7 +17,7 @@ import { } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IRecipe } from "warframe-public-export-plus"; @@ -105,7 +105,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ...updateCurrency(inventory, cost, true) }; } - if (recipe.secretIngredientAction != "SIA_UNBRAND") { + + if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { + const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!; + if (pet.Details!.HatchDate!.getTime() > Date.now()) { + pet.Details!.HatchDate = new Date(); + } + let canSetActive = true; + for (const pet of inventory.KubrowPets) { + if (pet.Details!.Status == Status.StatusAvailable) { + canSetActive = false; + break; + } + } + pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusIncubating; + } else if (recipe.secretIngredientAction != "SIA_UNBRAND") { InventoryChanges = { ...InventoryChanges, ...(await addItem( @@ -118,7 +132,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = )) }; } - if (config.claimingBlueprintRefundsIngredients) { + if ( + config.claimingBlueprintRefundsIngredients && + recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg + ) { await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe); } await inventory.save(); diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index 78adf1fc..8f68fc28 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -3,12 +3,14 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { getRecipe } from "@/src/services/itemDataService"; -import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addItem, addKubrowPet, 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"; -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { ExportWeapons } from "warframe-public-export-plus"; +import { getRandomElement } from "@/src/services/rngService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; interface IStartRecipeRequest { RecipeName: string; @@ -42,24 +44,35 @@ export const startRecipeController: RequestHandler = async (req, res) => { for (let i = 0; i != recipe.ingredients.length; ++i) { if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { - const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; - if (category != "LongGuns" && category != "Pistols" && category != "Melee") { - throw new Error(`unexpected equipment ingredient type: ${category}`); + if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { + const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); + if (index != -1) { + inventory.KubrowPetEggs!.splice(index, 1); + } + } else { + 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); + freeUpSlot(inventory, InventorySlot.WEAPONS); } - 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); - freeUpSlot(inventory, InventorySlot.WEAPONS); } else { await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); } } - if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { + let inventoryChanges: IInventoryChanges | undefined; + if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { + inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType); + pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId)); + } else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { const spectreLoadout: ISpectreLoadout = { ItemType: recipe.resultType, Suits: "", @@ -116,5 +129,5 @@ export const startRecipeController: RequestHandler = async (req, res) => { await inventory.save(); - res.json({ RecipeId: toOid(pr._id) }); + res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges }); }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 27caf691..2e4008dd 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1097,7 +1097,8 @@ const pendingRecipeSchema = new Schema( LongGuns: { type: [EquipmentSchema], default: undefined }, Pistols: { type: [EquipmentSchema], default: undefined }, Melee: { type: [EquipmentSchema], default: undefined }, - SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined } + SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }, + KubrowPet: { type: Schema.Types.ObjectId, default: undefined } }, { id: false } ); @@ -1115,6 +1116,7 @@ pendingRecipeSchema.set("toJSON", { delete returnedObject.Pistols; delete returnedObject.Melees; delete returnedObject.SuitToUnbrand; + delete returnedObject.KubrowPet; (returnedObject as IPendingRecipeClient).CompletionDate = { $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } }; diff --git a/src/routes/api.ts b/src/routes/api.ts index 7db66dfc..a12efbd0 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -9,6 +9,7 @@ import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserCo import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController"; import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; +import { adoptPetController } from "@/src/controllers/api/adoptPetController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; @@ -226,6 +227,7 @@ apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); apiRouter.post("/addPendingFriend.php", addPendingFriendController); apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); +apiRouter.post("/adoptPet.php", adoptPetController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/artifacts.php", artifactsController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 28832737..6060f1e9 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -86,6 +86,7 @@ import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService"; import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; import { TAccountDocument } from "./loginService"; +import { unixTimesInMs } from "../constants/timeConstants"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -780,7 +781,9 @@ export const addItem = async ( typeName.substr(1).split("/")[3] == "CatbrowPet" || typeName.substr(1).split("/")[3] == "KubrowPet" ) { - return addKubrowPet(inventory, typeName, undefined, premiumPurchase); + if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { + return addKubrowPet(inventory, typeName, undefined, premiumPurchase); + } } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { if (!seed) { throw new Error(`Expected crew member to have a seed`); @@ -1025,12 +1028,13 @@ export const addSpaceSuit = ( export const addKubrowPet = ( inventory: TInventoryDatabaseDocument, kubrowPetName: string, - details: IKubrowPetDetailsDatabase | undefined, - premiumPurchase: boolean, + details?: IKubrowPetDetailsDatabase, + premiumPurchase: boolean = false, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + // TODO: When incubating, this should only be given when claiming the recipe. const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined; const exalted = kubrowPet?.exalted ?? []; for (const specialItem of exalted) { @@ -1079,11 +1083,11 @@ export const addKubrowPet = ( details = { Name: "", - IsPuppy: false, + IsPuppy: !premiumPurchase, HasCollar: true, - PrintsRemaining: 2, - Status: Status.StatusStasis, - HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000), + PrintsRemaining: 3, + Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating, + HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start. IsMale: !!getRandomInt(0, 1), Size: getRandomInt(70, 100) / 100, DominantTraits: traits, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 32cac658..997b50f4 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -765,7 +765,8 @@ export interface IKubrowPetDetailsClient extends Omit { + extends Omit< + IPendingRecipeDatabase, + "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet" + > { CompletionDate: IMongoDate; } From b7c47b91ff1cbc259250c897989a46ed8627bcdf Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:46:00 -0700 Subject: [PATCH 28/30] chore: improve getItemCategoryByUniqueName (#2130) unused function, but might as well make it at least half decent Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2130 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/itemDataService.ts | 37 +++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 3b3e997c..8af8a756 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,5 +1,4 @@ import { IKeyChainRequest } from "@/src/types/requestTypes"; -import { getIndexAfter } from "@/src/helpers/stringHelpers"; import { dict_de, dict_en, @@ -53,20 +52,32 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => { return Object.values(ExportRecipes).find(x => x.resultType == resultType); }; -export const getItemCategoryByUniqueName = (uniqueName: string): string => { - //Lotus/Types/Items/MiscItems/PolymerBundle - - let splitWord = "Items/"; - if (!uniqueName.includes("/Items/")) { - splitWord = "/Types/"; +export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => { + if (uniqueName in ExportCustoms) { + return ExportCustoms[uniqueName].productCategory; } - - const index = getIndexAfter(uniqueName, splitWord); - if (index === -1) { - throw new Error(`error parsing item category ${uniqueName}`); + if (uniqueName in ExportDrones) { + return "Drones"; } - const category = uniqueName.substring(index).split("/")[0]; - return category; + if (uniqueName in ExportKeys) { + return "LevelKeys"; + } + if (uniqueName in ExportGear) { + return "Consumables"; + } + if (uniqueName in ExportResources) { + return ExportResources[uniqueName].productCategory; + } + if (uniqueName in ExportSentinels) { + return ExportSentinels[uniqueName].productCategory; + } + if (uniqueName in ExportWarframes) { + return ExportWarframes[uniqueName].productCategory; + } + if (uniqueName in ExportWeapons) { + return ExportWeapons[uniqueName].productCategory; + } + return undefined; }; export const getItemName = (uniqueName: string): string | undefined => { From 135b1e54fea8416519e9a5091f3c514bbea3d7fe Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:46:18 -0700 Subject: [PATCH 29/30] feat: classic lich guess history (#2129) Closes #2123 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2129 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 | 82 ++++++++++++++----- src/helpers/nemesisHelpers.ts | 55 +++++++++---- src/services/missionInventoryUpdateService.ts | 8 +- 3 files changed, 103 insertions(+), 42 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 1efd0023..8da8fe4d 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,12 +1,17 @@ import { version_compare } from "@/src/helpers/inventoryHelpers"; import { consumeModCharge, + decodeNemesisGuess, encodeNemesisGuess, getInfNodes, getKnifeUpgrade, getNemesisManifest, getNemesisPasscode, getNemesisPasscodeModTypes, + GUESS_CORRECT, + GUESS_INCORRECT, + GUESS_NEUTRAL, + GUESS_NONE, GUESS_WILDCARD, IKnifeResponse } from "@/src/helpers/nemesisHelpers"; @@ -98,18 +103,29 @@ export const nemesisController: RequestHandler = async (req, res) => { if (inventory.Nemesis!.Faction == "FC_INFESTATION") { const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; - - // Add to GuessHistory - const result1 = passcode == guess[0] ? 0 : 1; - const result2 = passcode == guess[1] ? 0 : 1; - const result3 = passcode == guess[2] ? 0 : 1; + const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT; + const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT; + const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT; inventory.Nemesis!.GuessHistory.push( - encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3) + encodeNemesisGuess([ + { + symbol: guess[0], + result: result1 + }, + { + symbol: guess[1], + result: result2 + }, + { + symbol: guess[2], + result: result3 + } + ]) ); // Increase antivirus if correct antivirus mod is installed const response: IKnifeResponse = {}; - if (result1 == 0 || result2 == 0 || result3 == 0) { + if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) { let antivirusGain = 5; const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); @@ -150,19 +166,47 @@ export const nemesisController: RequestHandler = async (req, res) => { await inventory.save(); res.json(response); } else { - let RankIncrease: number | undefined; - if (body.guess != GUESS_WILDCARD) { - const passcode = getNemesisPasscode(inventory.Nemesis!); - if (passcode[body.position] != body.guess) { - const manifest = getNemesisManifest(inventory.Nemesis!.manifest); - if (inventory.Nemesis!.Rank + 1 < manifest.systemIndexes.length) { - inventory.Nemesis!.Rank += 1; - RankIncrease = 1; - } - inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); - await inventory.save(); - } + // For first guess, create a new entry. + if (body.position == 0) { + inventory.Nemesis!.GuessHistory.push( + encodeNemesisGuess([ + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + }, + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + }, + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + } + ]) + ); } + + // Evaluate guess + const correct = + body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess; + + // Update entry + const guess = decodeNemesisGuess( + inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] + ); + guess[body.position].symbol = body.guess; + guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT; + inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess); + + // Increase rank if incorrect + let RankIncrease: number | undefined; + if (!correct) { + RankIncrease = 1; + const manifest = getNemesisManifest(inventory.Nemesis!.manifest); + inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1); + inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); + } + await inventory.save(); res.json({ RankIncrease }); } } else if ((req.query.mode as string) == "rs") { diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 0c2eebbe..8d0d8527 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti return passcode; }; -const reqiuemMods: readonly string[] = [ +const requiemMods: readonly string[] = [ "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", @@ -263,32 +263,51 @@ export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNeme const passcode = getNemesisPasscode(nemesis); return nemesis.Faction == "FC_INFESTATION" ? passcode.map(i => antivirusMods[i]) - : passcode.map(i => reqiuemMods[i]); + : passcode.map(i => requiemMods[i]); }; +// Symbols; 0-7 are the normal requiem mods. export const GUESS_NONE = 8; export const GUESS_WILDCARD = 9; -export const encodeNemesisGuess = ( - symbol1: number, - result1: number, - symbol2: number, - result2: number, - symbol3: number, - result3: number -): number => { +// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik. +export const GUESS_NEUTRAL = 0; +export const GUESS_INCORRECT = 1; +export const GUESS_CORRECT = 2; + +interface NemesisPositionGuess { + symbol: number; + result: number; +} + +export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess]; + +export const encodeNemesisGuess = (guess: NemesisGuess): number => { return ( - (symbol1 & 0xf) | - ((result1 & 3) << 12) | - ((symbol2 << 4) & 0xff) | - ((result2 << 14) & 0xffff) | - ((symbol3 & 0xf) << 8) | - ((result3 & 3) << 16) + (guess[0].symbol & 0xf) | + ((guess[0].result & 3) << 12) | + ((guess[1].symbol << 4) & 0xff) | + ((guess[1].result << 14) & 0xffff) | + ((guess[2].symbol & 0xf) << 8) | + ((guess[2].result & 3) << 16) ); }; -export const decodeNemesisGuess = (val: number): number[] => { - return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3]; +export const decodeNemesisGuess = (val: number): NemesisGuess => { + return [ + { + symbol: val & 0xf, + result: (val >> 12) & 3 + }, + { + symbol: (val & 0xff) >> 4, + result: (val & 0xffff) >> 14 + }, + { + symbol: (val >> 8) & 0xf, + result: (val >> 16) & 3 + } + ]; }; export interface IKnifeResponse { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 9c7dc93a..21aff3b1 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1182,14 +1182,12 @@ export const addMissionRewards = async ( if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1); if (inventory.Nemesis.InfNodes.length <= 0) { + const manifest = getNemesisManifest(inventory.Nemesis.manifest); if (inventory.Nemesis.Faction != "FC_INFESTATION") { - inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4); + inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, manifest.systemIndexes.length - 1); inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank; } - inventory.Nemesis.InfNodes = getInfNodes( - getNemesisManifest(inventory.Nemesis.manifest), - inventory.Nemesis.Rank - ); + inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank); } if (inventory.Nemesis.Faction == "FC_INFESTATION") { From f9a4d48b4daf9c9c350e61d53bb08ad14d191df8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:46:45 -0700 Subject: [PATCH 30/30] chore: base all cycles on locked time if used (#2128) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2128 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 9c887445..1dee4116 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -166,8 +166,8 @@ const microplanetEndlessJobs = [ const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 -const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { - return Date.now() + 300_000 > date; +const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => { + return nowMs + 300_000 > thenMs; }; const getSortieTime = (day: number): number => { @@ -939,14 +939,16 @@ const getCalendarSeason = (week: number): ICalendarSeason => { }; export const getWorldState = (buildLabel?: string): IWorldState => { - const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const timeSecs = config.worldState?.lockTime || Math.round(Date.now() / 1000); + const timeMs = timeSecs * 1000; + const day = Math.trunc((timeMs - EPOCH) / 86400000); const week = Math.trunc(day / 7); const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const worldState: IWorldState = { BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, - Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), + Time: timeSecs, Goals: [], Alerts: [], Sorties: [], @@ -1000,11 +1002,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2)); worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1)); worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0)); - if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); } pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1); } } @@ -1013,7 +1015,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff); // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - let bountyCycle = Math.trunc(Date.now() / 9000000); + let bountyCycle = Math.trunc(timeSecs / 9000); let bountyCycleEnd: number | undefined; do { const bountyCycleStart = bountyCycle * 9000000; @@ -1044,7 +1046,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); pushClassicBounties(worldState.SyndicateMissions, bountyCycle); - } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); + } while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ @@ -1087,15 +1089,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => { { const rollover = getSortieTime(day); - if (Date.now() < rollover) { + if (timeMs < rollover) { worldState.Sorties.push(getSortie(day - 1)); } - if (isBeforeNextExpectedWorldStateRefresh(rollover)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)) { worldState.Sorties.push(getSortie(day)); } // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. - const sdy = Date.now() >= rollover ? day : day - 1; + const sdy = timeMs >= rollover ? day : day - 1; const rng = new SRng(sdy); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate"); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate"); @@ -1107,7 +1109,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // Archon Hunt cycling every week worldState.LiteSorties.push(getLiteSortie(week)); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { worldState.LiteSorties.push(getLiteSortie(week + 1)); } @@ -1144,12 +1146,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // 1999 Calendar Season cycling every week + YearIteration every 4 weeks worldState.KnownCalendarSeasons.push(getCalendarSeason(week)); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); } // Sentient Anomaly cycling every 30 minutes - const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); + const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2)); const tmp = { cavabegin: "1690761600", PurchasePlatformLockEnabled: true,