chore: auto-generate guild advertisment vendor (#1845)
All checks were successful
Build Docker image / docker (push) Successful in 50s
Build / build (push) Successful in 1m33s

With this, preprocessing is simplified to just refreshing expiry dates. No real change to auto-generation logic.

Reviewed-on: #1845
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-04-25 15:12:45 -07:00 committed by Sainan
parent 3f6734ac1c
commit 90e97d7888
3 changed files with 32 additions and 157 deletions

View File

@ -1,13 +1,7 @@
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
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 { import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
IItemManifestPreprocessed,
IRawVendorManifest,
IVendorInfo,
IVendorInfoPreprocessed,
IVendorManifestPreprocessed
} from "@/src/types/vendorTypes";
import { ExportVendors, IRange } from "warframe-public-export-plus"; import { ExportVendors, IRange } from "warframe-public-export-plus";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
@ -24,7 +18,6 @@ import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorIn
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json";
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
@ -39,7 +32,7 @@ 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";
const rawVendorManifests: IRawVendorManifest[] = [ const rawVendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest, ArchimedeanVendorManifest,
DeimosEntratiFragmentVendorProductsManifest, DeimosEntratiFragmentVendorProductsManifest,
DeimosFishmongerVendorManifest, DeimosFishmongerVendorManifest,
@ -54,7 +47,6 @@ const rawVendorManifests: IRawVendorManifest[] = [
DuviriAcrithisVendorManifest, DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest, EntratiLabsEntratiLabVendorManifest,
GuildAdvertisementVendorManifest, // uses preprocessing
HubsIronwakeDondaVendorManifest, // uses preprocessing HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest, HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest, MaskSalesmanManifest,
@ -71,8 +63,8 @@ const rawVendorManifests: IRawVendorManifest[] = [
]; ];
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> { interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
cycleOffset: number; cycleOffset?: number;
cycleDuration: number; cycleDuration?: number;
} }
const generatableVendors: IGeneratableVendorInfo[] = [ const generatableVendors: IGeneratableVendorInfo[] = [
@ -99,9 +91,13 @@ const generatableVendors: IGeneratableVendorInfo[] = [
_id: { $oid: "5be4a159b144f3cdf1c22efa" }, _id: { $oid: "5be4a159b144f3cdf1c22efa" },
TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest",
PropertyTextHash: "A39621049CA3CA13761028CD21C239EF", PropertyTextHash: "A39621049CA3CA13761028CD21C239EF",
RandomSeedType: "VRST_FLAVOUR_TEXT", RandomSeedType: "VRST_FLAVOUR_TEXT"
cycleOffset: 1734307200_000, },
cycleDuration: unixTimesInMs.hour {
_id: { $oid: "61ba123467e5d37975aeeb03" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
PropertyTextHash: "255AFE2169BAE4130B4B20D7C55D14FA",
RandomSeedType: "VRST_FLAVOUR_TEXT"
} }
// { // {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" }, // _id: { $oid: "5dbb4c41e966f7886c3ce939" },
@ -110,7 +106,7 @@ const generatableVendors: IGeneratableVendorInfo[] = [
// } // }
]; ];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => { export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | 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);
@ -124,7 +120,7 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPr
return undefined; return undefined;
}; };
export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed | undefined => { export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
for (const vendorManifest of rawVendorManifests) { for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo._id.$oid == oid) { if (vendorManifest.VendorInfo._id.$oid == oid) {
return preprocessVendorManifest(vendorManifest); return preprocessVendorManifest(vendorManifest);
@ -138,29 +134,20 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed
return undefined; return undefined;
}; };
const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendorManifestPreprocessed => { const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
const manifest = structuredClone(originalManifest); const manifest = structuredClone(originalManifest);
const info = manifest.VendorInfo; const info = manifest.VendorInfo;
refreshExpiry(info.Expiry); refreshExpiry(info.Expiry);
for (const offer of info.ItemManifest) { for (const offer of info.ItemManifest) {
const iteration = refreshExpiry(offer.Expiry); 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;
} }
} return originalManifest;
}
return manifest as IVendorManifestPreprocessed;
}
return originalManifest as IVendorManifestPreprocessed;
}; };
const refreshExpiry = (expiry: IMongoDate): number => { const refreshExpiry = (expiry: IMongoDate): void => {
const period = parseInt(expiry.$date.$numberLong); const period = parseInt(expiry.$date.$numberLong);
if (Date.now() >= period) { if (Date.now() >= period) {
const epoch = 1734307200_000; // Monday (for weekly schedules) const epoch = 1734307200_000; // Monday (for weekly schedules)
@ -168,9 +155,7 @@ const refreshExpiry = (expiry: IMongoDate): number => {
const start = epoch + iteration * period; const start = epoch + iteration * period;
const end = start + period; const end = start + period;
expiry.$date.$numberLong = end.toString(); expiry.$date.$numberLong = end.toString();
return iteration;
} }
return 0;
}; };
const toRange = (value: IRange | number): IRange => { const toRange = (value: IRange | number): IRange => {
@ -180,9 +165,9 @@ const toRange = (value: IRange | number): IRange => {
return value; return value;
}; };
const vendorInfoCache: Record<string, IVendorInfoPreprocessed> = {}; const vendorInfoCache: Record<string, IVendorInfo> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
if (!(vendorInfo.TypeName in vendorInfoCache)) { if (!(vendorInfo.TypeName in vendorInfoCache)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
@ -205,7 +190,9 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
// Add new offers // Add new offers
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16); const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
const cycleIndex = Math.trunc((Date.now() - vendorInfo.cycleOffset) / vendorInfo.cycleDuration); const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
const cycleDuration = vendorInfo.cycleDuration ?? unixTimesInMs.hour;
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
const rng = new CRng(mixSeeds(vendorSeed, cycleIndex)); const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
const manifest = ExportVendors[vendorInfo.TypeName]; const manifest = ExportVendors[vendorInfo.TypeName];
const offersToAdd = []; const offersToAdd = [];
@ -226,14 +213,19 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
offersToAdd.push(rawItem); offersToAdd.push(rawItem);
} }
} }
// For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception.
if (!manifest.isOneBinPerCycle) {
offersToAdd.reverse();
} }
const cycleStart = vendorInfo.cycleOffset + cycleIndex * vendorInfo.cycleDuration; }
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) { for (const rawItem of offersToAdd) {
const durationHoursRange = toRange(rawItem.durationHours); const durationHoursRange = toRange(rawItem.durationHours);
const expiry = const expiry =
cycleStart + cycleStart +
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
const item: IItemManifestPreprocessed = { const item: IItemManifest = {
StoreItem: rawItem.storeItem, StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin, Bin: "BIN_" + rawItem.bin,

View File

@ -1,15 +1,11 @@
import { IMongoDate, IOid } from "./commonTypes"; import { IMongoDate, IOid } from "./commonTypes";
export interface IItemPrice { export interface IItemPrice {
ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. ItemType: string;
ItemCount: number; ItemCount: number;
ProductCategory: string; ProductCategory: string;
} }
export interface IItemPricePreprocessed extends Omit<IItemPrice, "ItemType"> {
ItemType: string;
}
export interface IItemManifest { export interface IItemManifest {
StoreItem: string; StoreItem: string;
ItemPrices?: IItemPrice[]; ItemPrices?: IItemPrice[];
@ -24,10 +20,6 @@ export interface IItemManifest {
Id: IOid; Id: IOid;
} }
export interface IItemManifestPreprocessed extends Omit<IItemManifest, "ItemPrices"> {
ItemPrices?: IItemPricePreprocessed[];
}
export interface IVendorInfo { export interface IVendorInfo {
_id: IOid; _id: IOid;
TypeName: string; TypeName: string;
@ -39,14 +31,6 @@ export 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.
} }
export interface IVendorInfoPreprocessed extends Omit<IVendorInfo, "ItemManifest"> { export interface IVendorManifest {
ItemManifest: IItemManifestPreprocessed[];
}
export interface IRawVendorManifest {
VendorInfo: IVendorInfo; VendorInfo: IVendorInfo;
} }
export interface IVendorManifestPreprocessed {
VendorInfo: IVendorInfoPreprocessed;
}

View File

@ -1,101 +0,0 @@
{
"VendorInfo": {
"_id": { "$oid": "61ba123467e5d37975aeeb03" },
"TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon",
"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": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 79554843,
"Id": { "$oid": "67bbb592e1534511d6c1c1e2" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain",
"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": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 2413820225,
"Id": { "$oid": "67bbb592e1534511d6c1c1e3" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm",
"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": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 3262300883,
"Id": { "$oid": "67bbb592e1534511d6c1c1e4" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow",
"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": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 2797325750,
"Id": { "$oid": "67bbb592e1534511d6c1c1e5" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost",
"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": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 554932310,
"Id": { "$oid": "67bbb592e1534511d6c1c1e6" }
}
],
"PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA",
"RandomSeedType": "VRST_FLAVOUR_TEXT",
"Expiry": { "$date": { "$numberLong": "604800000" } }
}
}