diff --git a/config.json.example b/config.json.example index 78eac7a1..72495c8b 100644 --- a/config.json.example +++ b/config.json.example @@ -41,6 +41,7 @@ "noVendorPurchaseLimits": false, "noDeathMarks": false, "noKimCooldowns": false, + "fullyStockedVendors": false, "syndicateMissionsRepeatable": false, "unlockAllProfitTakerStages": false, "instantFinishRivenChallenge": false, diff --git a/src/services/configService.ts b/src/services/configService.ts index 79d76d73..74b8f5b7 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -48,6 +48,7 @@ export interface IConfig { noVendorPurchaseLimits?: boolean; noDeathMarks?: boolean; noKimCooldowns?: boolean; + fullyStockedVendors?: boolean; syndicateMissionsRepeatable?: boolean; unlockAllProfitTakerStages?: boolean; instantFinishRivenChallenge?: boolean; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index c40c3cc6..d5ea7410 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -6,6 +6,7 @@ import { mixSeeds, SRng } from "@/src/services/rngService"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { logger } from "@/src/utils/logger"; import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus"; +import { config } from "./configService"; interface IGeneratableVendorInfo extends Omit { cycleOffset?: number; @@ -59,20 +60,23 @@ const getCycleDuration = (manifest: IVendor): number => { return dur * unixTimesInMs.hour; }; -export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { +export const getVendorManifestByTypeName = (typeName: string, fullStock?: boolean): IVendorManifest | undefined => { for (const vendorInfo of generatableVendors) { if (vendorInfo.TypeName == typeName) { - return generateVendorManifest(vendorInfo); + return generateVendorManifest(vendorInfo, fullStock ?? config.fullyStockedVendors); } } if (typeName in ExportVendors) { const manifest = ExportVendors[typeName]; - return generateVendorManifest({ - _id: { $oid: getVendorOid(typeName) }, - TypeName: typeName, - RandomSeedType: manifest.randomSeedType, - cycleDuration: getCycleDuration(manifest) - }); + return generateVendorManifest( + { + _id: { $oid: getVendorOid(typeName) }, + TypeName: typeName, + RandomSeedType: manifest.randomSeedType, + cycleDuration: getCycleDuration(manifest) + }, + fullStock ?? config.fullyStockedVendors + ); } return undefined; }; @@ -80,18 +84,21 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { for (const vendorInfo of generatableVendors) { if (vendorInfo._id.$oid == oid) { - return generateVendorManifest(vendorInfo); + return generateVendorManifest(vendorInfo, config.fullyStockedVendors); } } for (const [typeName, manifest] of Object.entries(ExportVendors)) { const typeNameOid = getVendorOid(typeName); if (typeNameOid == oid) { - return generateVendorManifest({ - _id: { $oid: typeNameOid }, - TypeName: typeName, - RandomSeedType: manifest.randomSeedType, - cycleDuration: getCycleDuration(manifest) - }); + return generateVendorManifest( + { + _id: { $oid: typeNameOid }, + TypeName: typeName, + RandomSeedType: manifest.randomSeedType, + cycleDuration: getCycleDuration(manifest) + }, + config.fullyStockedVendors + ); } } return undefined; @@ -169,9 +176,26 @@ const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => { } }; +let vendorManifestsUsingFullStock = false; const vendorManifestCache: Record = {}; -const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { +const clearVendorCache = (): void => { + for (const k of Object.keys(vendorManifestCache)) { + delete vendorManifestCache[k]; + } +}; + +const generateVendorManifest = ( + vendorInfo: IGeneratableVendorInfo, + fullStock: boolean | undefined +): IVendorManifest => { + fullStock ??= config.fullyStockedVendors; + fullStock ??= false; + if (vendorManifestsUsingFullStock != fullStock) { + vendorManifestsUsingFullStock = fullStock; + clearVendorCache(); + } + if (!(vendorInfo.TypeName in vendorManifestCache)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; @@ -208,7 +232,20 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani const cycleIndex = Math.trunc((now - cycleOffset) / cycleDuration); const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); const offersToAdd: IVendorOffer[] = []; - if (!manifest.isOneBinPerCycle) { + if (manifest.isOneBinPerCycle) { + if (fullStock) { + for (const rawItem of manifest.items) { + offersToAdd.push(rawItem); + } + } else { + const binThisCycle = cycleIndex % 2; // Note: May want to check the actual number of bins, but this is only used for coda weapons right now. + for (const rawItem of manifest.items) { + if (rawItem.bin == binThisCycle) { + offersToAdd.push(rawItem); + } + } + } + } else { // Compute vendor requirements, subtracting existing offers const remainingItemCapacity: Record = {}; const missingItemsPerBin: Record = {}; @@ -254,12 +291,14 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani manifest.numItems && (manifest.numItems.minValue != manifest.numItems.maxValue || manifest.numItems.minValue != numCountedOffers); - const numItemsTarget = manifest.numItems - ? numUncountedOffers + - (useRng - ? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue) - : manifest.numItems.minValue) - : manifest.items.length; + const numItemsTarget = fullStock + ? numUncountedOffers + numCountedOffers + : manifest.numItems + ? numUncountedOffers + + (useRng + ? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue) + : manifest.numItems.minValue) + : manifest.items.length; let i = 0; const rollableOffers = manifest.items.filter(x => x.probability !== undefined) as (Omit< IVendorOffer, @@ -282,13 +321,6 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani i = 0; } } - } else { - const binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. - for (const rawItem of manifest.items) { - if (rawItem.bin == binThisCycle) { - offersToAdd.push(rawItem); - } - } } const cycleStart = cycleOffset + cycleIndex * cycleDuration; for (const rawItem of offersToAdd) { @@ -387,34 +419,44 @@ if (args.dev) { logger.warn(`getCycleDuration self test failed`); } - const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! - .VendorInfo.ItemManifest; - if ( - ads.length != 5 || - ads[0].Bin != "BIN_4" || - ads[1].Bin != "BIN_3" || - ads[2].Bin != "BIN_2" || - ads[3].Bin != "BIN_1" || - ads[4].Bin != "BIN_0" - ) { - logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`); + for (let i = 0; i != 2; ++i) { + const fullStock = !!i; + + const ads = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", + fullStock + )!.VendorInfo.ItemManifest; + if ( + ads.length != 5 || + ads[0].Bin != "BIN_4" || + ads[1].Bin != "BIN_3" || + ads[2].Bin != "BIN_2" || + ads[3].Bin != "BIN_1" || + ads[4].Bin != "BIN_0" + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`); + } + + const pall = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest", + fullStock + )!.VendorInfo.ItemManifest; + if ( + pall.length != 5 || + pall[0].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament" || + pall[1].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" || + pall[2].StoreItem != "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits" || + pall[3].StoreItem != "/Lotus/StoreItems/Types/Items/MiscItems/Kuva" || + pall[4].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`); + } } - const pall = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest")! - .VendorInfo.ItemManifest; - if ( - pall.length != 5 || - pall[0].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament" || - pall[1].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" || - pall[2].StoreItem != "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits" || - pall[3].StoreItem != "/Lotus/StoreItems/Types/Items/MiscItems/Kuva" || - pall[4].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" - ) { - logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`); - } - - const cms = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest")! - .VendorInfo.ItemManifest; + const cms = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest", + false + )!.VendorInfo.ItemManifest; if ( cms.length != 9 || cms[0].Bin != "BIN_2" || @@ -426,13 +468,15 @@ if (args.dev) { logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`); } - const temple = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest")! - .VendorInfo.ItemManifest; + const temple = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest", + false + )!.VendorInfo.ItemManifest; if (!temple.find(x => x.StoreItem == "/Lotus/StoreItems/Types/Items/MiscItems/Kuva")) { logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest`); } - const nakak = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest")! + const nakak = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest", false)! .VendorInfo.ItemManifest; if ( nakak.length != 10 || diff --git a/static/webui/index.html b/static/webui/index.html index dca46e9d..5eb4b297 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -700,6 +700,10 @@ +
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 6958e7ed..b82f547f 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -158,6 +158,7 @@ dict = { cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_noDeathMarks: `Keine Todesmarkierungen`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, + cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`, cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`, cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`, cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 04fbe563..870f1d89 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -157,6 +157,7 @@ dict = { cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_noDeathMarks: `No Death Marks`, cheats_noKimCooldowns: `No KIM Cooldowns`, + cheats_fullyStockedVendors: `Fully Stocked Vendors`, cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`, cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`, cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 5f1e470d..eb56f094 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -158,6 +158,7 @@ dict = { cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, cheats_noDeathMarks: `Sin marcas de muerte`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, + cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`, cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`, cheats_unlockAllProfitTakerStages: `Deslobquea todas las etapas del Roba-ganancias`, cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 0c23c61a..5d522b2b 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -158,6 +158,7 @@ dict = { cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`, cheats_noDeathMarks: `Aucune marque d'assassin`, cheats_noKimCooldowns: `Aucun cooldown sur le KIM`, + cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`, cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`, cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`, cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 22d873bc..e06907bf 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -158,6 +158,7 @@ dict = { cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_noDeathMarks: `Без меток сметри`, cheats_noKimCooldowns: `Чаты KIM без кулдауна`, + cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`, cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`, cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`, cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 303a1599..8e2e9344 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -158,6 +158,7 @@ dict = { cheats_noVendorPurchaseLimits: `商城或商人无购买限制`, cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`, cheats_noKimCooldowns: `无 KIM 冷却时间`, + cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`, cheats_syndicateMissionsRepeatable: `集团任务可重复`, cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`, cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,