From cccf6f04a549bbab9df8606d158461f4b6159a5a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:32:34 -0700 Subject: [PATCH 01/10] feat: xtra cheese rotation (#2191) This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2191 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 253bdc21..a19abee5 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1297,8 +1297,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => { pushVoidStorms(worldState.VoidStorms, hour); } - // Sentient Anomaly cycling every 30 minutes + // Sentient Anomaly + Xtra Cheese cycles const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2)); + const hourInSeconds = 3600; + const cheeseInterval = hourInSeconds * 8; + const cheeseDuration = hourInSeconds * 2; + const cheeseIndex = Math.trunc(timeSecs / cheeseInterval); const tmp = { cavabegin: "1690761600", PurchasePlatformLockEnabled: true, @@ -1323,6 +1327,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }, ennnd: true, mbrt: true, + fbst: { + a: cheeseIndex * cheeseInterval, // This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live. + e: cheeseIndex * cheeseInterval + cheeseDuration, + n: (cheeseIndex + 1) * hourInSeconds * 8 + }, sfn: [550, 553, 554, 555][halfHour % 4] }; worldState.Tmp = JSON.stringify(tmp); From a9f1368cb7dcc3156ee20bdc150fc33737ce27c7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 17 Jun 2025 20:58:10 -0700 Subject: [PATCH 02/10] fix: add UpgradeType field to repaired railjack weapons (#2193) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2193 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 49d96b70..97abbecf 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -11,7 +11,7 @@ import { scaleRequiredCount, setGuildTechLogState } from "@/src/services/guildService"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addCrewShipWeaponSkin, @@ -442,6 +442,7 @@ const finishComponentRepair = ( ...(category == "CrewShipWeaponSkins" ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) : addEquipment(inventory, category, salvageItem.ItemType, { + UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType, UpgradeFingerprint: salvageItem.UpgradeFingerprint })), ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) From ffeffe2796e193a2145b97374cb0c9733412cb96 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Wed, 18 Jun 2025 05:18:02 -0700 Subject: [PATCH 03/10] chore(webui): update es (#2195) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2195 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 0b6b15ec..e07199ca 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -3,8 +3,8 @@ dict = { general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`, general_addButton: `Agregar`, general_bulkActions: `Acciones masivas`, - code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, - code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, + code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`, + code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`, code_nonValidAuthz: `Tus credenciales no son válidas.`, code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`, code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`, From 3dcd2663d3d01418018fe00f6c499c9cb956c527 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:39:10 -0700 Subject: [PATCH 04/10] fix: weaken classic lich when getting all 3 mods correct (#2186) Fixes #2185 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2186 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 | 59 +++++++++++++++++------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index fe536267..56cc5562 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -17,6 +17,7 @@ import { IKnifeResponse } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { getAccountForRequest } from "@/src/services/loginService"; @@ -202,16 +203,28 @@ export const nemesisController: RequestHandler = async (req, res) => { 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 response: INemesisRequiemResponse = {}; + if (correct) { + if (body.position == 2) { + // That was all 3 guesses correct, nemesis is now weakened. + inventory.Nemesis!.InfNodes = [ + { + Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + await consumePasscodeModCharges(inventory, response); + } + } else { + // Guess was incorrect, increase rank + response.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 }); + res.json(response); } } else if ((req.query.mode as string) == "rs") { // report spawn; POST but no application data in body @@ -299,20 +312,11 @@ export const nemesisController: RequestHandler = async (req, res) => { ]; inventory.Nemesis!.Weakened = true; - const response: IKnifeResponse & { target: INemesisClient } = { + const response: INemesisWeakenResponse = { target: inventory.toJSON().Nemesis! }; - // Consume charge of the correct requiem mod(s) - const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; - const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); - const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; - const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; - const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!); - for (const modType of modTypes) { - const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType); - consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); - } + await consumePasscodeModCharges(inventory, response); await inventory.save(); res.json(response); @@ -370,11 +374,19 @@ interface INemesisRequiemRequest { knife?: IKnife; } +interface INemesisRequiemResponse extends IKnifeResponse { + RankIncrease?: number; +} + // interface INemesisWeakenRequest { // target: INemesisClient; // knife: IKnife; // } +interface INemesisWeakenResponse extends IKnifeResponse { + target: INemesisClient; +} + interface IKnife { Item: IEquipmentClient; Skins: IWeaponSkinClient[]; @@ -383,3 +395,18 @@ interface IKnife { AttachedUpgrades: IUpgradeClient[]; HiddenWhenHolstered: boolean; } + +const consumePasscodeModCharges = async ( + inventory: TInventoryDatabaseDocument, + response: IKnifeResponse +): Promise => { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!); + for (const modType of modTypes) { + const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType); + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + } +}; From 6691d4e402eeb5c033b08e741e74e07f2c0eb63a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:49:58 -0700 Subject: [PATCH 05/10] feat: autogenerate steel path honors vendor (#2187) No more "preprocessing" needed now. Some good progress for #1225, I'd say. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2187 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 | 143 +++-- .../TeshinHardModeVendorManifest.json | 603 ------------------ 4 files changed, 79 insertions(+), 677 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json diff --git a/package-lock.json b/package-lock.json index ceccfce4..5d1f4a36 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.67", + "warframe-public-export-plus": "^0.5.68", "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.67", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.67.tgz", - "integrity": "sha512-LsnZD2E5PTA+5MK9kDGvM/hFDtg8sb0EwQ4hKH5ILqrSgz30a9W8785v77RSsL1AEVF8dfb/lZcSTCJq1DZHzQ==" + "version": "0.5.68", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.68.tgz", + "integrity": "sha512-KMmwCVeQ4k+EN73UZqxnM+qQdPsST8geWoJCP7US5LT6JcRxa8ptmqYXwCzaLtckBLZyVbamsxKZAxPPJckxsA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index eaff654a..b8d00e39 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.67", + "warframe-public-export-plus": "^0.5.68", "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 295759b2..1c947197 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -3,7 +3,6 @@ import { isDev } from "@/src/helpers/pathHelper"; import { catBreadHash } from "@/src/helpers/stringHelpers"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { mixSeeds, SRng } from "@/src/services/rngService"; -import { IMongoDate } from "@/src/types/commonTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { logger } from "@/src/utils/logger"; import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus"; @@ -25,7 +24,6 @@ import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json"; -import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; const rawVendorManifests: IVendorManifest[] = [ @@ -46,7 +44,6 @@ const rawVendorManifests: IVendorManifest[] = [ OstronPetVendorManifest, SolarisDebtTokenVendorRepossessionsManifest, Temple1999VendorManifest, - TeshinHardModeVendorManifest, // uses preprocessing ZarimanCommisionsManifestArchimedean ]; @@ -87,12 +84,16 @@ const gcd = (a: number, b: number): number => { const getCycleDuration = (manifest: IVendor): number => { let dur = 0; for (const item of manifest.items) { - if (typeof item.durationHours != "number") { + if (item.alwaysOffered) { + continue; + } + const durationHours = item.rotatedWeekly ? 168 : item.durationHours; + if (typeof durationHours != "number") { dur = 1; break; } - if (dur != item.durationHours) { - dur = gcd(dur, item.durationHours); + if (dur != durationHours) { + dur = gcd(dur, durationHours); } } return dur * unixTimesInMs.hour; @@ -101,7 +102,7 @@ const getCycleDuration = (manifest: IVendor): number => { export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { - return preprocessVendorManifest(vendorManifest); + return vendorManifest; } } for (const vendorInfo of generatableVendors) { @@ -124,7 +125,7 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo._id.$oid == oid) { - return preprocessVendorManifest(vendorManifest); + return vendorManifest; } } for (const vendorInfo of generatableVendors) { @@ -183,30 +184,6 @@ export const applyStandingToVendorManifest = ( }; }; -const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => { - if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { - const manifest = structuredClone(originalManifest); - const info = manifest.VendorInfo; - refreshExpiry(info.Expiry); - for (const offer of info.ItemManifest) { - refreshExpiry(offer.Expiry); - } - return manifest; - } - return originalManifest; -}; - -const refreshExpiry = (expiry: IMongoDate): void => { - const period = parseInt(expiry.$date.$numberLong); - if (Date.now() >= period) { - const epoch = 1734307200_000; // Monday (for weekly schedules) - const iteration = Math.trunc((Date.now() - epoch) / period); - const start = epoch + iteration * period; - const end = start + period; - expiry.$date.$numberLong = end.toString(); - } -}; - const toRange = (value: IRange | number): IRange => { if (typeof value == "number") { return { minValue: value, maxValue: value }; @@ -230,6 +207,18 @@ const getCycleDurationRange = (manifest: IVendor): IRange | undefined => { return res.maxValue != 0 ? res : undefined; }; +type TOfferId = string; + +const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => { + if ("storeItem" in offer) { + // IVendorOffer + return offer.storeItem + "x" + offer.quantity; + } else { + // IItemManifest + return offer.StoreItem + "x" + offer.QuantityMultiplier; + } +}; + const vendorManifestCache: Record = {}; const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { @@ -270,7 +259,8 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); const offersToAdd: IVendorOffer[] = []; if (!manifest.isOneBinPerCycle) { - const remainingItemCapacity: Record = {}; + // Compute vendor requirements, subtracting existing offers + const remainingItemCapacity: Record = {}; const missingItemsPerBin: Record = {}; let numOffersThatNeedToMatchABin = 0; if (manifest.numItemsPerBin) { @@ -280,56 +270,59 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani } } for (const item of manifest.items) { - remainingItemCapacity[item.storeItem] = 1 + item.duplicates; + remainingItemCapacity[getOfferId(item)] = 1 + item.duplicates; } for (const offer of info.ItemManifest) { - remainingItemCapacity[offer.StoreItem] -= 1; + remainingItemCapacity[getOfferId(offer)] -= 1; const bin = parseInt(offer.Bin.substring(4)); if (missingItemsPerBin[bin]) { missingItemsPerBin[bin] -= 1; numOffersThatNeedToMatchABin -= 1; } } - if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) { - const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); + + // Add permanent offers + let numUncountedOffers = 0; + let offset = 0; + for (const item of manifest.items) { + if (item.alwaysOffered || item.rotatedWeekly) { + ++numUncountedOffers; + const id = getOfferId(item); + if (remainingItemCapacity[id] != 0) { + remainingItemCapacity[id] -= 1; + offersToAdd.push(item); + ++offset; + } + } + } + + // Add counted offers + if (manifest.numItems) { + const useRng = manifest.numItems.minValue != manifest.numItems.maxValue; + const numItemsTarget = + numUncountedOffers + + (useRng + ? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue) + : manifest.numItems.minValue); + let i = 0; while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) { - // TODO: Consider item probability weightings - const item = rng.randomElement(manifest.items)!; + const item = useRng ? rng.randomElement(manifest.items)! : manifest.items[i++]; if ( - remainingItemCapacity[item.storeItem] != 0 && + !item.alwaysOffered && + remainingItemCapacity[getOfferId(item)] != 0 && (numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin]) ) { - remainingItemCapacity[item.storeItem] -= 1; + remainingItemCapacity[getOfferId(item)] -= 1; if (missingItemsPerBin[item.bin]) { missingItemsPerBin[item.bin] -= 1; numOffersThatNeedToMatchABin -= 1; } - offersToAdd.push(item); + offersToAdd.splice(offset, 0, item); + } + if (i == manifest.items.length) { + i = 0; } } - } else { - for (const item of manifest.items) { - if (!item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) { - remainingItemCapacity[item.storeItem] -= 1; - offersToAdd.push(item); - } - } - for (const e of Object.entries(remainingItemCapacity)) { - const item = manifest.items.find(x => x.storeItem == e[0])!; - if (!item.alwaysOffered) { - while (e[1] != 0) { - e[1] -= 1; - offersToAdd.push(item); - } - } - } - for (const item of manifest.items) { - if (item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) { - remainingItemCapacity[item.storeItem] -= 1; - offersToAdd.push(item); - } - } - offersToAdd.reverse(); } } else { const binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. @@ -342,16 +335,21 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const cycleStart = cycleOffset + cycleIndex * cycleDuration; for (const rawItem of offersToAdd) { const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration); - const expiry = - cycleStart + - rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; + const expiry = rawItem.alwaysOffered + ? 2051240400_000 + : cycleStart + + (rawItem.rotatedWeekly + ? unixTimesInMs.week + : rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour); const item: IItemManifest = { StoreItem: rawItem.storeItem, ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), Bin: "BIN_" + rawItem.bin, QuantityMultiplier: rawItem.quantity, Expiry: { $date: { $numberLong: expiry.toString() } }, - AllowMultipurchase: false, + PurchaseQuantityLimit: rawItem.purchaseLimit, + RotatedWeekly: rawItem.rotatedWeekly, + AllowMultipurchase: rawItem.purchaseLimit !== 1, Id: { $oid: ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + @@ -422,6 +420,13 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani }; if (isDev) { + if ( + getCycleDuration(ExportVendors["/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest"]) != + unixTimesInMs.week + ) { + logger.warn(`getCycleDuration self test failed`); + } + const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! .VendorInfo.ItemManifest; if ( diff --git a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json deleted file mode 100644 index 7934f0a3..00000000 --- a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json +++ /dev/null @@ -1,603 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "63ed01efbdaa38891767bac9" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinArmsBlueprint", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9947" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinBodyBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9948" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinHeadBlueprint", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9949" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinLegsBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e994a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e994b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e994c" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Components/FormaStanceBlueprint", - "ItemPrices": [ - { - "ItemCount": 10, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e994d" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Effects/OrbsEphemera", - "ItemPrices": [ - { - "ItemCount": 3, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e994e" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Effects/TatsuSkullEphemera", - "ItemPrices": [ - { - "ItemCount": 85, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e994f" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawShotgunRandomMod", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9950" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Components/UmbraFormaBlueprint", - "ItemPrices": [ - { - "ItemCount": 150, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9951" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva", - "ItemPrices": [ - { - "ItemCount": 55, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 50000, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9952" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawModularPistolRandomMod", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9953" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Forma", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 3, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9954" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawModularMeleeRandomMod", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9955" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/EvergreenLoginRewardFusionBundle", - "ItemPrices": [ - { - "ItemCount": 150, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9956" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e9957" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/WeaponRecoilReductionMod", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9958" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/TeshinBobbleHead", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9959" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageGaussVED", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e995a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageGrendelVED", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e995b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageProteaAction", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e995c" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/TeaSet", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e995d" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageXakuAction", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e995e" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/RivenIdentifier", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "RotatedWeekly": true, - "AllowMultipurchase": false, - "Id": { - "$oid": "66fd60b20ba592c4c95e995f" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/RandomSyndicateProjectionPack", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "604800000" - } - }, - "PurchaseQuantityLimit": 25, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e997c" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 10000, - "Expiry": { - "$date": { - "$numberLong": "604800000" - } - }, - "PurchaseQuantityLimit": 25, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e997d" - } - } - ], - "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D", - "Expiry": { - "$date": { - "$numberLong": "604800000" - } - } - } -} From 16e80acb533e10665eaf8b334c559b9988c2ec6c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:50:43 -0700 Subject: [PATCH 06/10] chore: add return type to createMessage (#2188) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2188 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inboxService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index 0c2d698d..ea837b01 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -50,14 +50,17 @@ export const createNewEventMessages = async (req: Request): Promise => { await account.save(); }; -export const createMessage = async (accountId: string | Types.ObjectId, messages: IMessageCreationTemplate[]) => { +export const createMessage = async ( + accountId: string | Types.ObjectId, + messages: IMessageCreationTemplate[] +): Promise[]> => { const ownerIdMessages = messages.map(m => ({ ...m, ownerId: accountId })); const savedMessages = await Inbox.insertMany(ownerIdMessages); - return savedMessages; + return savedMessages as HydratedDocument[]; }; export interface IMessageCreationTemplate extends Omit { From f8d0c9e0cbeb6db86fd105b071314e157b1b362c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:51:20 -0700 Subject: [PATCH 07/10] chore: use ChallengesFixVersion that client provides (#2190) It seems that now we're on version 7, so let's just not hard-code it. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2190 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/updateChallengeProgressController.ts | 6 +++++- src/models/inventoryModels/inventoryModel.ts | 2 +- src/types/inventoryTypes/inventoryTypes.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index b948bb79..51eb88c3 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -11,8 +11,11 @@ export const updateChallengeProgressController: RequestHandler = async (req, res const inventory = await getInventory( account._id.toString(), - "ChallengeProgress SeasonChallengeHistory Affiliations" + "ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations" ); + if (challenges.ChallengesFixVersion !== undefined) { + inventory.ChallengesFixVersion = challenges.ChallengesFixVersion; + } let affiliationMods: IAffiliationMods[] = []; if (challenges.ChallengeProgress) { affiliationMods = addChallenges( @@ -40,6 +43,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res }; interface IUpdateChallengeProgressRequest { + ChallengesFixVersion?: number; ChallengeProgress?: IChallengeProgress[]; SeasonChallengeHistory?: ISeasonChallenge[]; SeasonChallengeCompletions?: ISeasonChallenge[]; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a9335fa7..0daeb2c1 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1703,7 +1703,7 @@ const inventorySchema = new Schema( LastInventorySync: Schema.Types.ObjectId, Mailbox: MailboxSchema, HandlerPoints: Number, - ChallengesFixVersion: { type: Number, default: 6 }, + ChallengesFixVersion: Number, PlayedParkourTutorial: Boolean, //ActiveLandscapeTraps: [Schema.Types.Mixed], //RepVotes: [Schema.Types.Mixed], diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 94149f7c..c3a8a7d1 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -234,7 +234,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu HandlerPoints: number; MiscItems: IMiscItem[]; HasOwnedVoidProjectionsPreviously?: boolean; - ChallengesFixVersion: number; + ChallengesFixVersion?: number; ChallengeProgress: IChallengeProgress[]; RawUpgrades: IRawUpgrade[]; ReceivedStartingGear: boolean; From 9af0e06b70b98d7c4edb1776871c63dee4fed85f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:51:56 -0700 Subject: [PATCH 08/10] feat: add worldState.circuitGameModes config option (#2192) Closes #749 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2192 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- README.md | 1 + config.json.example | 3 ++- src/services/configService.ts | 1 + src/services/worldStateService.ts | 12 ++++++--- src/types/worldStateTypes.ts | 45 +++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e8d2b419..dc57e796 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,4 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi - `RadioLegion2Syndicate` for The Emissary - `RadioLegionIntermissionSyndicate` for Intermission I - `RadioLegionSyndicate` for The Wolf of Saturn Six +- `worldState.circuitGameModes` can be provided with an array of valid game modes (`Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, `Alchemy`) diff --git a/config.json.example b/config.json.example index bf6eae91..8f839751 100644 --- a/config.json.example +++ b/config.json.example @@ -58,7 +58,8 @@ "starDays": true, "eidolonOverride": "", "vallisOverride": "", - "nightwaveOverride": "" + "nightwaveOverride": "", + "circuitGameModes": null }, "dev": { "keepVendorsExpired": false diff --git a/src/services/configService.ts b/src/services/configService.ts index 82cc4f31..fbcda8af 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -65,6 +65,7 @@ interface IConfig { eidolonOverride?: string; vallisOverride?: string; nightwaveOverride?: string; + circuitGameModes?: string[]; }; dev?: { keepVendorsExpired?: boolean; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index a19abee5..5f2df857 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -16,8 +16,10 @@ import { ISortie, ISortieMission, ISyndicateMissionInfo, + ITmp, IVoidStorm, - IWorldState + IWorldState, + TCircuitGameMode } from "../types/worldStateTypes"; import { version_compare } from "../helpers/inventoryHelpers"; import { logger } from "../utils/logger"; @@ -1303,10 +1305,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { const cheeseInterval = hourInSeconds * 8; const cheeseDuration = hourInSeconds * 2; const cheeseIndex = Math.trunc(timeSecs / cheeseInterval); - const tmp = { + const tmp: ITmp = { cavabegin: "1690761600", PurchasePlatformLockEnabled: true, - tcsn: true, pgr: { ts: "1732572900", en: "CUSTOM DECALS @ ZEVILA", @@ -1330,10 +1331,13 @@ export const getWorldState = (buildLabel?: string): IWorldState => { fbst: { a: cheeseIndex * cheeseInterval, // This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live. e: cheeseIndex * cheeseInterval + cheeseDuration, - n: (cheeseIndex + 1) * hourInSeconds * 8 + n: (cheeseIndex + 1) * cheeseInterval }, sfn: [550, 553, 554, 555][halfHour % 4] }; + if (Array.isArray(config.worldState?.circuitGameModes)) { + tmp.edg = config.worldState.circuitGameModes as TCircuitGameMode[]; + } worldState.Tmp = JSON.stringify(tmp); return worldState; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 880c5800..bd8ab138 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -191,3 +191,48 @@ export interface ICalendarEvent { dialogueName?: string; dialogueConvo?: string; } + +export type TCircuitGameMode = + | "Survival" + | "VoidFlood" + | "Excavation" + | "Defense" + | "Exterminate" + | "Assassination" + | "Alchemy"; + +export interface ITmp { + cavabegin: string; + PurchasePlatformLockEnabled: boolean; // Seems unused + pgr: IPgr; + ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0) + mbrt?: boolean; // Related to mobile app rating request + fbst: IFbst; + sfn: number; + edg?: TCircuitGameMode[]; // The Circuit game modes overwrite +} + +interface IPgr { + ts: string; + en: string; + fr: string; + it: string; + de: string; + es: string; + pt: string; + ru: string; + pl: string; + uk: string; + tr: string; + ja: string; + zh: string; + ko: string; + tc: string; + th: string; +} + +interface IFbst { + a: number; + e: number; + n: number; +} From b8b8b6a6c6975b2d1f745f98809b70065782e924 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:52:16 -0700 Subject: [PATCH 09/10] feat: railjack valence fusion (#2194) Closes #1678 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2194 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipFusionController.ts | 107 ++++++++++++++++++ src/routes/api.ts | 2 + 2 files changed, 109 insertions(+) create mode 100644 src/controllers/api/crewShipFusionController.ts diff --git a/src/controllers/api/crewShipFusionController.ts b/src/controllers/api/crewShipFusionController.ts new file mode 100644 index 00000000..87cfd2ce --- /dev/null +++ b/src/controllers/api/crewShipFusionController.ts @@ -0,0 +1,107 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { ICrewShipComponentFingerprint, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; + +export const crewShipFusionController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = getJSONfromString(String(req.body)); + + const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid); + const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!; + const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins"; + const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; + const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!; + const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65; + const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65; + + const inventoryChanges: IInventoryChanges = {}; + + // Charge partial repair cost if fusing with an identified but unrepaired part + if (payload.SourceRecipe) { + const recipe = ExportDojoRecipes.research[payload.SourceRecipe]; + updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges); + const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) })); + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + // Remove inferior item + if (payload.SourceRecipe) { + inventory[salvageCategory].pull({ _id: payload.PartB.$oid }); + inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }]; + } else { + const inferiorId = tierA < tierB ? payload.PartA : payload.PartB; + inventory[category].pull({ _id: inferiorId.$oid }); + inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }]; + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 }; + } + + // Upgrade superior item + const superiorItem = tierA < tierB ? itemB : itemA; + const inferiorItem = tierA < tierB ? itemA : itemB; + const fingerprint: ICrewShipComponentFingerprint = JSON.parse( + superiorItem.UpgradeFingerprint! + ) as ICrewShipComponentFingerprint; + const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint + ? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint) + : { compat: "", buffs: [] }; + if (isWeapon) { + for (let i = 0; i != fingerprint.buffs.length; ++i) { + const buffA = fingerprint.buffs[i]; + const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined; + const fvalA = buffA.Value / 0x3fffffff; + const fvalB = (buffB?.Value ?? 0) / 0x3fffffff; + const percA = 0.3 + fvalA * (0.6 - 0.3); + const percB = 0.3 + fvalB * (0.6 - 0.3); + const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]); + const newFval = (newPerc - 0.3) / (0.6 - 0.3); + buffA.Value = Math.trunc(newFval * 0x3fffffff); + } + } else { + const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? []; + const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? []; + for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) { + const buffA = fingerprint.buffs[i]; + const buffB = inferiorFingerprint.buffs[i]; + const fvalA = buffA.Value / 0x3fffffff; + const fvalB = buffB.Value / 0x3fffffff; + const rangeA = superiorMeta[i].range; + const rangeB = inferiorMeta[i].range; + const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]); + const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]); + const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]); + const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]); + buffA.Value = Math.trunc(newFval * 0x3fffffff); + } + if (inferiorFingerprint.SubroutineIndex) { + const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA; + if (!useSuperiorSubroutine) { + fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex; + } + } + } + superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inventoryChanges[category] = [superiorItem.toJSON() as any]; + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); +}; + +interface ICrewShipFusionRequest { + PartA: IOid; + PartB: IOid; + SourceRecipe: string; + UseSubroutineA: boolean; +} + +const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02]; diff --git a/src/routes/api.ts b/src/routes/api.ts index 8b0a12bc..df9e8b59 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -33,6 +33,7 @@ import { createAllianceController } from "@/src/controllers/api/createAllianceCo import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { crewMembersController } from "@/src/controllers/api/crewMembersController"; +import { crewShipFusionController } from "@/src/controllers/api/crewShipFusionController"; import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; @@ -247,6 +248,7 @@ apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/crewMembers.php", crewMembersController); +apiRouter.post("/crewShipFusion.php", crewShipFusionController); apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); From 7819d87bbeb777a8570f941d37078e3108bed12c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:23:39 -0700 Subject: [PATCH 10/10] chore: update nodejs version in Dockerfile (#2199) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2199 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c36da824..b5b957e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine3.19 +FROM node:24-alpine3.21 ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF ENV APP_MY_ADDRESS=localhost