From 6a1e508109de0c421afaaafb84cdb7b1c0d28bcb Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 29 Mar 2025 09:27:56 -0700 Subject: [PATCH] feat: initial vendor rotations (#1360) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1360 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/getVendorInfoController.ts | 4 +- src/services/leaderboardService.ts | 3 +- src/services/purchaseService.ts | 27 ++++++-- src/services/serversideVendorsService.ts | 64 ++++++++++++------- src/types/vendorTypes.ts | 46 +++++++++++++ .../GuildAdvertisementVendorManifest.json | 52 +++++++++++---- .../HubsIronwakeDondaVendorManifest.json | 12 ++-- .../TeshinHardModeVendorManifest.json | 6 +- 8 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 src/types/vendorTypes.ts diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts index b161176e..c7212550 100644 --- a/src/controllers/api/getVendorInfoController.ts +++ b/src/controllers/api/getVendorInfoController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; export const getVendorInfoController: RequestHandler = (req, res) => { if (typeof req.query.vendor == "string") { @@ -7,7 +7,7 @@ export const getVendorInfoController: RequestHandler = (req, res) => { if (!manifest) { throw new Error(`Unknown vendor: ${req.query.vendor}`); } - res.json(manifest); + res.json(preprocessVendorManifest(manifest)); } else { res.status(400).end(); } diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts index c4084d6e..b0e03518 100644 --- a/src/services/leaderboardService.ts +++ b/src/services/leaderboardService.ts @@ -15,8 +15,7 @@ export const submitLeaderboardScore = async ( expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); } else { const EPOCH = 1734307200 * 1000; // Monday - const day = Math.trunc((Date.now() - EPOCH) / 86400000); - const week = Math.trunc(day / 7); + const week = Math.trunc((Date.now() - EPOCH) / 604800000); const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; expiry = new Date(weekEnd); diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 7f0170d2..f2b71427 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -9,7 +9,7 @@ import { updateSlots } from "@/src/services/inventoryService"; import { getRandomWeightedRewardUc } from "@/src/services/rngService"; -import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; +import { getVendorManifestByOid, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; @@ -52,8 +52,9 @@ export const handlePurchase = async ( const prePurchaseInventoryChanges: IInventoryChanges = {}; if (purchaseRequest.PurchaseParams.Source == 7) { - const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); - if (manifest) { + const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); + if (rawManifest) { + const manifest = preprocessVendorManifest(rawManifest); let ItemId: string | undefined; if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) @@ -87,16 +88,28 @@ export const handlePurchase = async ( }) - 1 ]; } + let expiry = parseInt(offer.Expiry.$date.$numberLong); + if (purchaseRequest.PurchaseParams.IsWeekly) { + const EPOCH = 1734307200 * 1000; // Monday + const week = Math.trunc((Date.now() - EPOCH) / 604800000); + const weekStart = EPOCH + week * 604800000; + expiry = weekStart + 604800000; + } const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); let numPurchased = purchaseRequest.PurchaseParams.Quantity; if (historyEntry) { - numPurchased += historyEntry.NumPurchased; - historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; + if (Date.now() >= historyEntry.Expiry.getTime()) { + historyEntry.NumPurchased = numPurchased; + historyEntry.Expiry = new Date(expiry); + } else { + numPurchased += historyEntry.NumPurchased; + historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; + } } else { vendorPurchases.PurchaseHistory.push({ ItemId: ItemId, NumPurchased: purchaseRequest.PurchaseParams.Quantity, - Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) + Expiry: new Date(expiry) }); } prePurchaseInventoryChanges.NewVendorPurchase = { @@ -105,7 +118,7 @@ export const handlePurchase = async ( { ItemId: ItemId, NumPurchased: numPurchased, - Expiry: offer.Expiry + Expiry: { $date: { $numberLong: expiry.toString() } } } ] }; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 79e22ef3..37a3425c 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,4 +1,6 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { CRng, mixSeeds } from "@/src/services/rngService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; @@ -31,25 +33,6 @@ import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorI import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; -interface IVendorManifest { - VendorInfo: { - _id: IOid; - TypeName: string; - ItemManifest: { - StoreItem: string; - ItemPrices?: { ItemType: string; ItemCount: number; ProductCategory: string }[]; - Bin: string; - QuantityMultiplier: number; - Expiry: IMongoDate; - PurchaseQuantityLimit?: number; - RotatedWeekly?: boolean; - AllowMultipurchase: boolean; - Id: IOid; - }[]; - Expiry: IMongoDate; - }; -} - const vendorManifests: IVendorManifest[] = [ ArchimedeanVendorManifest, DeimosEntratiFragmentVendorProductsManifest, @@ -65,8 +48,8 @@ const vendorManifests: IVendorManifest[] = [ DuviriAcrithisVendorManifest, EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabVendorManifest, - GuildAdvertisementVendorManifest, - HubsIronwakeDondaVendorManifest, + GuildAdvertisementVendorManifest, // uses preprocessing + HubsIronwakeDondaVendorManifest, // uses preprocessing HubsPerrinSequenceWeaponVendorManifest, HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, @@ -79,7 +62,7 @@ const vendorManifests: IVendorManifest[] = [ SolarisDebtTokenVendorRepossessionsManifest, SolarisFishmongerVendorManifest, SolarisProspectorVendorManifest, - TeshinHardModeVendorManifest, + TeshinHardModeVendorManifest, // uses preprocessing ZarimanCommisionsManifestArchimedean ]; @@ -100,3 +83,38 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined } return undefined; }; + +export const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifestPreprocessed => { + if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { + const manifest = structuredClone(originalManifest); + const info = manifest.VendorInfo; + refreshExpiry(info.Expiry); + for (const offer of info.ItemManifest) { + const iteration = refreshExpiry(offer.Expiry); + if (offer.ItemPrices) { + for (const price of offer.ItemPrices) { + if (typeof price.ItemType != "string") { + const itemSeed = parseInt(offer.Id.$oid.substring(16), 16); + const rng = new CRng(mixSeeds(itemSeed, iteration)); + price.ItemType = rng.randomElement(price.ItemType); + } + } + } + } + return manifest as IVendorManifestPreprocessed; + } + return originalManifest as IVendorManifestPreprocessed; +}; + +const refreshExpiry = (expiry: IMongoDate): number => { + const period = parseInt(expiry.$date.$numberLong); + if (Date.now() >= period) { + const epoch = 1734307200 * 1000; // Monday (for weekly schedules) + const iteration = Math.trunc((Date.now() - epoch) / period); + const start = epoch + iteration * period; + const end = start + period; + expiry.$date.$numberLong = end.toString(); + return iteration; + } + return 0; +}; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts new file mode 100644 index 00000000..d7dbd749 --- /dev/null +++ b/src/types/vendorTypes.ts @@ -0,0 +1,46 @@ +import { IMongoDate, IOid } from "./commonTypes"; + +interface IItemPrice { + ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. + ItemCount: number; + ProductCategory: string; +} + +interface IItemPricePreprocessed extends Omit { + ItemType: string; +} + +interface IItemManifest { + StoreItem: string; + ItemPrices?: IItemPrice[]; + Bin: string; + QuantityMultiplier: number; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. + PurchaseQuantityLimit?: number; + RotatedWeekly?: boolean; + AllowMultipurchase: boolean; + Id: IOid; +} + +interface IItemManifestPreprocessed extends Omit { + ItemPrices?: IItemPricePreprocessed[]; +} + +interface IVendorInfo { + _id: IOid; + TypeName: string; + ItemManifest: IItemManifest[]; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. +} + +interface IVendorInfoPreprocessed extends Omit { + ItemManifest: IItemManifestPreprocessed[]; +} + +export interface IVendorManifest { + VendorInfo: IVendorInfo; +} + +export interface IVendorManifestPreprocessed { + VendorInfo: IVendorInfoPreprocessed; +} diff --git a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json index 05681d38..20e3e3a3 100644 --- a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json @@ -5,11 +5,17 @@ "ItemManifest": [ { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 12, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 12, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_4", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 79554843, @@ -17,11 +23,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 7, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 7, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_3", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 2413820225, @@ -29,11 +41,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 3, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 3, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_2", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 3262300883, @@ -41,11 +59,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 20, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 20, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_1", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 2797325750, @@ -53,11 +77,17 @@ }, { "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost", - "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 10, "ProductCategory": "MiscItems" }], + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 10, + "ProductCategory": "MiscItems" + } + ], "RegularPrice": [1, 1], "Bin": "BIN_0", "QuantityMultiplier": 1, - "Expiry": { "$date": { "$numberLong": "9999999000000" } }, + "Expiry": { "$date": { "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, "AllowMultipurchase": false, "LocTagRandSeed": 554932310, @@ -66,6 +96,6 @@ ], "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA", "RandomSeedType": "VRST_FLAVOUR_TEXT", - "Expiry": { "$date": { "$numberLong": "9999999000000" } } + "Expiry": { "$date": { "$numberLong": "604800000" } } } } diff --git a/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json index 0dabeb95..bec20cc1 100644 --- a/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json @@ -18,7 +18,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "AllowMultipurchase": true, @@ -39,7 +39,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -61,7 +61,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -83,7 +83,7 @@ "QuantityMultiplier": 35000, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -105,7 +105,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -118,7 +118,7 @@ "PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621", "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } } } diff --git a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json index 4572855f..7934f0a3 100644 --- a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json @@ -561,7 +561,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 25, @@ -583,7 +583,7 @@ "QuantityMultiplier": 10000, "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 25, @@ -596,7 +596,7 @@ "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D", "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } } }