feat: initial vendor rotations
All checks were successful
Build / build (22) (push) Successful in 1m18s
Build / build (18) (pull_request) Successful in 43s
Build / build (20) (push) Successful in 42s
Build / build (18) (push) Successful in 1m17s
Build / build (20) (pull_request) Successful in 1m14s
Build / build (22) (pull_request) Successful in 1m18s

This commit is contained in:
Sainan 2025-03-28 23:38:51 +01:00
parent 3a904753f2
commit bd4ab2fcac
8 changed files with 160 additions and 54 deletions

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService";
export const getVendorInfoController: RequestHandler = (req, res) => { export const getVendorInfoController: RequestHandler = (req, res) => {
if (typeof req.query.vendor == "string") { if (typeof req.query.vendor == "string") {
@ -7,7 +7,7 @@ export const getVendorInfoController: RequestHandler = (req, res) => {
if (!manifest) { if (!manifest) {
throw new Error(`Unknown vendor: ${req.query.vendor}`); throw new Error(`Unknown vendor: ${req.query.vendor}`);
} }
res.json(manifest); res.json(preprocessVendorManifest(manifest));
} else { } else {
res.status(400).end(); res.status(400).end();
} }

View File

@ -15,8 +15,7 @@ export const submitLeaderboardScore = async (
expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000);
} else { } else {
const EPOCH = 1734307200 * 1000; // Monday const EPOCH = 1734307200 * 1000; // Monday
const day = Math.trunc((Date.now() - EPOCH) / 86400000); const week = Math.trunc((Date.now() - EPOCH) / 604800000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000; const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000; const weekEnd = weekStart + 604800000;
expiry = new Date(weekEnd); expiry = new Date(weekEnd);

View File

@ -9,7 +9,7 @@ import {
updateSlots updateSlots
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getRandomWeightedRewardUc } from "@/src/services/rngService"; 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 { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
@ -52,8 +52,9 @@ export const handlePurchase = async (
const prePurchaseInventoryChanges: IInventoryChanges = {}; const prePurchaseInventoryChanges: IInventoryChanges = {};
if (purchaseRequest.PurchaseParams.Source == 7) { if (purchaseRequest.PurchaseParams.Source == 7) {
const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
if (manifest) { if (rawManifest) {
const manifest = preprocessVendorManifest(rawManifest);
let ItemId: string | undefined; let ItemId: string | undefined;
if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
@ -87,16 +88,28 @@ export const handlePurchase = async (
}) - 1 }) - 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); const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
let numPurchased = purchaseRequest.PurchaseParams.Quantity; let numPurchased = purchaseRequest.PurchaseParams.Quantity;
if (historyEntry) { if (historyEntry) {
numPurchased += historyEntry.NumPurchased; if (Date.now() >= historyEntry.Expiry.getTime()) {
historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; historyEntry.NumPurchased = numPurchased;
historyEntry.Expiry = new Date(expiry);
} else {
numPurchased += historyEntry.NumPurchased;
historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity;
}
} else { } else {
vendorPurchases.PurchaseHistory.push({ vendorPurchases.PurchaseHistory.push({
ItemId: ItemId, ItemId: ItemId,
NumPurchased: purchaseRequest.PurchaseParams.Quantity, NumPurchased: purchaseRequest.PurchaseParams.Quantity,
Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong)) Expiry: new Date(expiry)
}); });
} }
prePurchaseInventoryChanges.NewVendorPurchase = { prePurchaseInventoryChanges.NewVendorPurchase = {
@ -105,7 +118,7 @@ export const handlePurchase = async (
{ {
ItemId: ItemId, ItemId: ItemId,
NumPurchased: numPurchased, NumPurchased: numPurchased,
Expiry: offer.Expiry Expiry: { $date: { $numberLong: expiry.toString() } }
} }
] ]
}; };

View File

@ -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 ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.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 TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.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[] = [ const vendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest, ArchimedeanVendorManifest,
DeimosEntratiFragmentVendorProductsManifest, DeimosEntratiFragmentVendorProductsManifest,
@ -65,8 +48,8 @@ const vendorManifests: IVendorManifest[] = [
DuviriAcrithisVendorManifest, DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest, EntratiLabsEntratiLabVendorManifest,
GuildAdvertisementVendorManifest, GuildAdvertisementVendorManifest, // uses preprocessing
HubsIronwakeDondaVendorManifest, HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsPerrinSequenceWeaponVendorManifest, HubsPerrinSequenceWeaponVendorManifest,
HubsRailjackCrewMemberVendorManifest, HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest, MaskSalesmanManifest,
@ -79,7 +62,7 @@ const vendorManifests: IVendorManifest[] = [
SolarisDebtTokenVendorRepossessionsManifest, SolarisDebtTokenVendorRepossessionsManifest,
SolarisFishmongerVendorManifest, SolarisFishmongerVendorManifest,
SolarisProspectorVendorManifest, SolarisProspectorVendorManifest,
TeshinHardModeVendorManifest, TeshinHardModeVendorManifest, // uses preprocessing
ZarimanCommisionsManifestArchimedean ZarimanCommisionsManifestArchimedean
]; ];
@ -100,3 +83,38 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined
} }
return 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;
};

46
src/types/vendorTypes.ts Normal file
View File

@ -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<IItemPrice, "ItemType"> {
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<IItemManifest, "ItemPrices"> {
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<IVendorInfo, "ItemManifest"> {
ItemManifest: IItemManifestPreprocessed[];
}
export interface IVendorManifest {
VendorInfo: IVendorInfo;
}
export interface IVendorManifestPreprocessed {
VendorInfo: IVendorInfoPreprocessed;
}

View File

@ -5,11 +5,17 @@
"ItemManifest": [ "ItemManifest": [
{ {
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon", "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], "RegularPrice": [1, 1],
"Bin": "BIN_4", "Bin": "BIN_4",
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } }, "Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
"AllowMultipurchase": false, "AllowMultipurchase": false,
"LocTagRandSeed": 79554843, "LocTagRandSeed": 79554843,
@ -17,11 +23,17 @@
}, },
{ {
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain", "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], "RegularPrice": [1, 1],
"Bin": "BIN_3", "Bin": "BIN_3",
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } }, "Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
"AllowMultipurchase": false, "AllowMultipurchase": false,
"LocTagRandSeed": 2413820225, "LocTagRandSeed": 2413820225,
@ -29,11 +41,17 @@
}, },
{ {
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm", "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], "RegularPrice": [1, 1],
"Bin": "BIN_2", "Bin": "BIN_2",
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } }, "Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
"AllowMultipurchase": false, "AllowMultipurchase": false,
"LocTagRandSeed": 3262300883, "LocTagRandSeed": 3262300883,
@ -41,11 +59,17 @@
}, },
{ {
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow", "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], "RegularPrice": [1, 1],
"Bin": "BIN_1", "Bin": "BIN_1",
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } }, "Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
"AllowMultipurchase": false, "AllowMultipurchase": false,
"LocTagRandSeed": 2797325750, "LocTagRandSeed": 2797325750,
@ -53,11 +77,17 @@
}, },
{ {
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost", "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], "RegularPrice": [1, 1],
"Bin": "BIN_0", "Bin": "BIN_0",
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } }, "Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
"AllowMultipurchase": false, "AllowMultipurchase": false,
"LocTagRandSeed": 554932310, "LocTagRandSeed": 554932310,
@ -66,6 +96,6 @@
], ],
"PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA", "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA",
"RandomSeedType": "VRST_FLAVOUR_TEXT", "RandomSeedType": "VRST_FLAVOUR_TEXT",
"Expiry": { "$date": { "$numberLong": "9999999000000" } } "Expiry": { "$date": { "$numberLong": "604800000" } }
} }
} }

View File

@ -18,7 +18,7 @@
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "9999999000000" "$numberLong": "604800000"
} }
}, },
"AllowMultipurchase": true, "AllowMultipurchase": true,
@ -39,7 +39,7 @@
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "9999999000000" "$numberLong": "604800000"
} }
}, },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
@ -61,7 +61,7 @@
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "9999999000000" "$numberLong": "604800000"
} }
}, },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
@ -83,7 +83,7 @@
"QuantityMultiplier": 35000, "QuantityMultiplier": 35000,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "9999999000000" "$numberLong": "604800000"
} }
}, },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
@ -105,7 +105,7 @@
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "9999999000000" "$numberLong": "604800000"
} }
}, },
"PurchaseQuantityLimit": 1, "PurchaseQuantityLimit": 1,
@ -118,7 +118,7 @@
"PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621", "PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621",
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "9999999000000" "$numberLong": "604800000"
} }
} }
} }

View File

@ -561,7 +561,7 @@
"QuantityMultiplier": 1, "QuantityMultiplier": 1,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "2051240400000" "$numberLong": "604800000"
} }
}, },
"PurchaseQuantityLimit": 25, "PurchaseQuantityLimit": 25,
@ -583,7 +583,7 @@
"QuantityMultiplier": 10000, "QuantityMultiplier": 10000,
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "2051240400000" "$numberLong": "604800000"
} }
}, },
"PurchaseQuantityLimit": 25, "PurchaseQuantityLimit": 25,
@ -596,7 +596,7 @@
"PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D", "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D",
"Expiry": { "Expiry": {
"$date": { "$date": {
"$numberLong": "2051240400000" "$numberLong": "604800000"
} }
} }
} }