chore: slightly generalise auto-generation of vendor manifests #1611

Merged
Sainan merged 1 commits from auto-vendors into main 2025-04-14 07:13:36 -07:00
3 changed files with 93 additions and 38 deletions

View File

@ -109,6 +109,12 @@ export class CRng {
randomReward<T extends { probability: number }>(pool: T[]): T | undefined { randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.random()); return getRewardAtPercentage(pool, this.random());
} }
churnSeed(its: number): void {
while (its--) {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
}
}
} }
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth. // Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.

View File

@ -3,9 +3,15 @@ import path from "path";
import { repoDir } from "@/src/helpers/pathHelper"; import { repoDir } from "@/src/helpers/pathHelper";
import { CRng, mixSeeds } from "@/src/services/rngService"; import { CRng, mixSeeds } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifestPreprocessed, IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; import {
IItemManifestPreprocessed,
IRawVendorManifest,
IVendorInfo,
IVendorManifestPreprocessed
} from "@/src/types/vendorTypes";
import { JSONParse } from "json-with-bigint"; import { JSONParse } from "json-with-bigint";
import { ExportVendors } from "warframe-public-export-plus"; import { ExportVendors } from "warframe-public-export-plus";
import { unixTimesInMs } from "../constants/timeConstants";
const getVendorManifestJson = (name: string): IRawVendorManifest => { const getVendorManifestJson = (name: string): IRawVendorManifest => {
return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8"));
@ -44,14 +50,37 @@ const rawVendorManifests: IRawVendorManifest[] = [
getVendorManifestJson("ZarimanCommisionsManifestArchimedean") getVendorManifestJson("ZarimanCommisionsManifestArchimedean")
]; ];
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
cycleDuration?: number;
}
const generatableVendors: IGeneratableVendorInfo[] = [
{
_id: { $oid: "67dadc30e4b6e0e5979c8d84" },
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56",
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleDuration: 4 * unixTimesInMs.day
}
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest",
// PropertyTextHash: "62B64A8065B7C0FA345895D4BC234621"
// }
];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => { export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => {
for (const vendorManifest of rawVendorManifests) { for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) { if (vendorManifest.VendorInfo.TypeName == typeName) {
return preprocessVendorManifest(vendorManifest); return preprocessVendorManifest(vendorManifest);
} }
} }
if (typeName == "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest") { for (const vendorInfo of generatableVendors) {
return generateCodaWeaponVendorManifest(); if (vendorInfo.TypeName == typeName) {
return generateVendorManifest(vendorInfo);
}
} }
return undefined; return undefined;
}; };
@ -62,8 +91,10 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed
return preprocessVendorManifest(vendorManifest); return preprocessVendorManifest(vendorManifest);
} }
} }
if (oid == "67dadc30e4b6e0e5979c8d84") { for (const vendorInfo of generatableVendors) {
return generateCodaWeaponVendorManifest(); if (vendorInfo._id.$oid == oid) {
return generateVendorManifest(vendorInfo);
}
} }
return undefined; return undefined;
}; };
@ -103,41 +134,59 @@ const refreshExpiry = (expiry: IMongoDate): number => {
return 0; return 0;
}; };
const generateCodaWeaponVendorManifest = (): IVendorManifestPreprocessed => { const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => {
const EPOCH = 1740960000 * 1000; const EPOCH = 1740960000 * 1000; // Monday; aligns with coda weapons 8 day cycle.
const DUR = 4 * 86400 * 1000; const manifest = ExportVendors[vendorInfo.TypeName];
const cycle = Math.trunc((Date.now() - EPOCH) / DUR); let binThisCycle;
const cycleStart = EPOCH + cycle * DUR; if (manifest.isOneBinPerCycle) {
const cycleEnd = cycleStart + DUR; const cycleDuration = vendorInfo.cycleDuration!; // manifest.items[0].durationHours! * 3600_000;
const binThisCycle = cycle % 2; // isOneBinPerCycle const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration);
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
}
const items: IItemManifestPreprocessed[] = []; const items: IItemManifestPreprocessed[] = [];
const manifest = ExportVendors["/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest"]; let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
const rng = new CRng(cycle); for (let i = 0; i != manifest.items.length; ++i) {
for (const rawItem of manifest.items) { const rawItem = manifest.items[i];
if (rawItem.bin != binThisCycle) { if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) {
continue; continue;
} }
items.push({ const cycleDuration = vendorInfo.cycleDuration!; // rawItem.durationHours! * 3600_000;
const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration);
const cycleStart = EPOCH + cycleIndex * cycleDuration;
const cycleEnd = cycleStart + cycleDuration;
if (soonestOfferExpiry > cycleEnd) {
soonestOfferExpiry = cycleEnd;
}
const rng = new CRng(cycleIndex);
rng.churnSeed(i);
/*for (let j = -1; j != rawItem.duplicates; ++j)*/ {
const item: IItemManifestPreprocessed = {
StoreItem: rawItem.storeItem, StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices!.map(item => ({ ...item, ProductCategory: "MiscItems" })), ItemPrices: rawItem.itemPrices!.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin, Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1, QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: cycleEnd.toString() } }, Expiry: { $date: { $numberLong: cycleEnd.toString() } },
AllowMultipurchase: false, AllowMultipurchase: false,
LocTagRandSeed: (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)), Id: {
Id: { $oid: "67e9da12793a120d" + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") } $oid:
}); i.toString(16).padStart(8, "0") +
vendorInfo._id.$oid.substring(8, 16) +
rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0")
} }
};
if (vendorInfo.RandomSeedType) {
item.LocTagRandSeed =
(BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff));
}
items.push(item);
}
}
delete vendorInfo.cycleDuration;
return { return {
VendorInfo: { VendorInfo: {
_id: { $oid: "67dadc30e4b6e0e5979c8d84" }, ...vendorInfo,
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
ItemManifest: items, ItemManifest: items,
PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56", Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } }
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
Expiry: { $date: { $numberLong: cycleEnd.toString() } }
} }
}; };
}; };

View File

@ -1,16 +1,16 @@
import { IMongoDate, IOid } from "./commonTypes"; import { IMongoDate, IOid } from "./commonTypes";
interface IItemPrice { export interface IItemPrice {
ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period.
ItemCount: number; ItemCount: number;
ProductCategory: string; ProductCategory: string;
} }
interface IItemPricePreprocessed extends Omit<IItemPrice, "ItemType"> { export interface IItemPricePreprocessed extends Omit<IItemPrice, "ItemType"> {
ItemType: string; ItemType: string;
} }
interface IItemManifest { export interface IItemManifest {
StoreItem: string; StoreItem: string;
ItemPrices?: IItemPrice[]; ItemPrices?: IItemPrice[];
Bin: string; Bin: string;
@ -27,7 +27,7 @@ export interface IItemManifestPreprocessed extends Omit<IItemManifest, "ItemPric
ItemPrices?: IItemPricePreprocessed[]; ItemPrices?: IItemPricePreprocessed[];
} }
interface IVendorInfo { export interface IVendorInfo {
_id: IOid; _id: IOid;
TypeName: string; TypeName: string;
ItemManifest: IItemManifest[]; ItemManifest: IItemManifest[];
@ -38,7 +38,7 @@ interface IVendorInfo {
Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
} }
interface IVendorInfoPreprocessed extends Omit<IVendorInfo, "ItemManifest"> { export interface IVendorInfoPreprocessed extends Omit<IVendorInfo, "ItemManifest"> {
ItemManifest: IItemManifestPreprocessed[]; ItemManifest: IItemManifestPreprocessed[];
} }