feat: fullyStockedVendors cheat (#2246)
All checks were successful
Build / build (push) Successful in 53s
Build Docker image / docker-amd64 (push) Successful in 1m8s
Build Docker image / docker-arm64 (push) Successful in 1m18s

Reviewed-on: #2246
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-06-22 06:55:44 -07:00 committed by Sainan
parent 7ca7147b78
commit 84f081312b
10 changed files with 115 additions and 59 deletions

View File

@ -41,6 +41,7 @@
"noVendorPurchaseLimits": false, "noVendorPurchaseLimits": false,
"noDeathMarks": false, "noDeathMarks": false,
"noKimCooldowns": false, "noKimCooldowns": false,
"fullyStockedVendors": false,
"syndicateMissionsRepeatable": false, "syndicateMissionsRepeatable": false,
"unlockAllProfitTakerStages": false, "unlockAllProfitTakerStages": false,
"instantFinishRivenChallenge": false, "instantFinishRivenChallenge": false,

View File

@ -48,6 +48,7 @@ export interface IConfig {
noVendorPurchaseLimits?: boolean; noVendorPurchaseLimits?: boolean;
noDeathMarks?: boolean; noDeathMarks?: boolean;
noKimCooldowns?: boolean; noKimCooldowns?: boolean;
fullyStockedVendors?: boolean;
syndicateMissionsRepeatable?: boolean; syndicateMissionsRepeatable?: boolean;
unlockAllProfitTakerStages?: boolean; unlockAllProfitTakerStages?: boolean;
instantFinishRivenChallenge?: boolean; instantFinishRivenChallenge?: boolean;

View File

@ -6,6 +6,7 @@ import { mixSeeds, SRng } from "@/src/services/rngService";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus"; import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus";
import { config } from "./configService";
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> { interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
cycleOffset?: number; cycleOffset?: number;
@ -59,20 +60,23 @@ const getCycleDuration = (manifest: IVendor): number => {
return dur * unixTimesInMs.hour; 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) { for (const vendorInfo of generatableVendors) {
if (vendorInfo.TypeName == typeName) { if (vendorInfo.TypeName == typeName) {
return generateVendorManifest(vendorInfo); return generateVendorManifest(vendorInfo, fullStock ?? config.fullyStockedVendors);
} }
} }
if (typeName in ExportVendors) { if (typeName in ExportVendors) {
const manifest = ExportVendors[typeName]; const manifest = ExportVendors[typeName];
return generateVendorManifest({ return generateVendorManifest(
_id: { $oid: getVendorOid(typeName) }, {
TypeName: typeName, _id: { $oid: getVendorOid(typeName) },
RandomSeedType: manifest.randomSeedType, TypeName: typeName,
cycleDuration: getCycleDuration(manifest) RandomSeedType: manifest.randomSeedType,
}); cycleDuration: getCycleDuration(manifest)
},
fullStock ?? config.fullyStockedVendors
);
} }
return undefined; return undefined;
}; };
@ -80,18 +84,21 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest |
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
for (const vendorInfo of generatableVendors) { for (const vendorInfo of generatableVendors) {
if (vendorInfo._id.$oid == oid) { if (vendorInfo._id.$oid == oid) {
return generateVendorManifest(vendorInfo); return generateVendorManifest(vendorInfo, config.fullyStockedVendors);
} }
} }
for (const [typeName, manifest] of Object.entries(ExportVendors)) { for (const [typeName, manifest] of Object.entries(ExportVendors)) {
const typeNameOid = getVendorOid(typeName); const typeNameOid = getVendorOid(typeName);
if (typeNameOid == oid) { if (typeNameOid == oid) {
return generateVendorManifest({ return generateVendorManifest(
_id: { $oid: typeNameOid }, {
TypeName: typeName, _id: { $oid: typeNameOid },
RandomSeedType: manifest.randomSeedType, TypeName: typeName,
cycleDuration: getCycleDuration(manifest) RandomSeedType: manifest.randomSeedType,
}); cycleDuration: getCycleDuration(manifest)
},
config.fullyStockedVendors
);
} }
} }
return undefined; return undefined;
@ -169,9 +176,26 @@ const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => {
} }
}; };
let vendorManifestsUsingFullStock = false;
const vendorManifestCache: Record<string, IVendorManifest> = {}; const vendorManifestCache: Record<string, IVendorManifest> = {};
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)) { if (!(vendorInfo.TypeName in vendorManifestCache)) {
// 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;
@ -208,7 +232,20 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
const cycleIndex = Math.trunc((now - cycleOffset) / cycleDuration); const cycleIndex = Math.trunc((now - cycleOffset) / cycleDuration);
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
const offersToAdd: IVendorOffer[] = []; 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 // Compute vendor requirements, subtracting existing offers
const remainingItemCapacity: Record<TOfferId, number> = {}; const remainingItemCapacity: Record<TOfferId, number> = {};
const missingItemsPerBin: Record<number, number> = {}; const missingItemsPerBin: Record<number, number> = {};
@ -254,12 +291,14 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
manifest.numItems && manifest.numItems &&
(manifest.numItems.minValue != manifest.numItems.maxValue || (manifest.numItems.minValue != manifest.numItems.maxValue ||
manifest.numItems.minValue != numCountedOffers); manifest.numItems.minValue != numCountedOffers);
const numItemsTarget = manifest.numItems const numItemsTarget = fullStock
? numUncountedOffers + ? numUncountedOffers + numCountedOffers
(useRng : manifest.numItems
? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue) ? numUncountedOffers +
: manifest.numItems.minValue) (useRng
: manifest.items.length; ? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue)
: manifest.numItems.minValue)
: manifest.items.length;
let i = 0; let i = 0;
const rollableOffers = manifest.items.filter(x => x.probability !== undefined) as (Omit< const rollableOffers = manifest.items.filter(x => x.probability !== undefined) as (Omit<
IVendorOffer, IVendorOffer,
@ -282,13 +321,6 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
i = 0; 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; const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) { for (const rawItem of offersToAdd) {
@ -387,34 +419,44 @@ if (args.dev) {
logger.warn(`getCycleDuration self test failed`); logger.warn(`getCycleDuration self test failed`);
} }
const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! for (let i = 0; i != 2; ++i) {
.VendorInfo.ItemManifest; const fullStock = !!i;
if (
ads.length != 5 || const ads = getVendorManifestByTypeName(
ads[0].Bin != "BIN_4" || "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
ads[1].Bin != "BIN_3" || fullStock
ads[2].Bin != "BIN_2" || )!.VendorInfo.ItemManifest;
ads[3].Bin != "BIN_1" || if (
ads[4].Bin != "BIN_0" ads.length != 5 ||
) { ads[0].Bin != "BIN_4" ||
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`); 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")! const cms = getVendorManifestByTypeName(
.VendorInfo.ItemManifest; "/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest",
if ( false
pall.length != 5 || )!.VendorInfo.ItemManifest;
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;
if ( if (
cms.length != 9 || cms.length != 9 ||
cms[0].Bin != "BIN_2" || cms[0].Bin != "BIN_2" ||
@ -426,13 +468,15 @@ if (args.dev) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`); logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`);
} }
const temple = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest")! const temple = getVendorManifestByTypeName(
.VendorInfo.ItemManifest; "/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest",
false
)!.VendorInfo.ItemManifest;
if (!temple.find(x => x.StoreItem == "/Lotus/StoreItems/Types/Items/MiscItems/Kuva")) { if (!temple.find(x => x.StoreItem == "/Lotus/StoreItems/Types/Items/MiscItems/Kuva")) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest`); 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; .VendorInfo.ItemManifest;
if ( if (
nakak.length != 10 || nakak.length != 10 ||

View File

@ -700,6 +700,10 @@
<input class="form-check-input" type="checkbox" id="noKimCooldowns" /> <input class="form-check-input" type="checkbox" id="noKimCooldowns" />
<label class="form-check-label" for="noKimCooldowns" data-loc="cheats_noKimCooldowns"></label> <label class="form-check-label" for="noKimCooldowns" data-loc="cheats_noKimCooldowns"></label>
</div> </div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fullyStockedVendors" />
<label class="form-check-label" for="fullyStockedVendors" data-loc="cheats_fullyStockedVendors"></label>
</div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="syndicateMissionsRepeatable" /> <input class="form-check-input" type="checkbox" id="syndicateMissionsRepeatable" />
<label class="form-check-label" for="syndicateMissionsRepeatable" data-loc="cheats_syndicateMissionsRepeatable"></label> <label class="form-check-label" for="syndicateMissionsRepeatable" data-loc="cheats_syndicateMissionsRepeatable"></label>

View File

@ -158,6 +158,7 @@ dict = {
cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`,
cheats_noDeathMarks: `Keine Todesmarkierungen`, cheats_noDeathMarks: `Keine Todesmarkierungen`,
cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`,
cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`,
cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`, cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`,
cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`, cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`,
cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`, cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`,

View File

@ -157,6 +157,7 @@ dict = {
cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`,
cheats_noDeathMarks: `No Death Marks`, cheats_noDeathMarks: `No Death Marks`,
cheats_noKimCooldowns: `No KIM Cooldowns`, cheats_noKimCooldowns: `No KIM Cooldowns`,
cheats_fullyStockedVendors: `Fully Stocked Vendors`,
cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`, cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`,
cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`, cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`,
cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`, cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`,

View File

@ -158,6 +158,7 @@ dict = {
cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
cheats_noDeathMarks: `Sin marcas de muerte`, cheats_noDeathMarks: `Sin marcas de muerte`,
cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`,
cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`,
cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`, cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`,
cheats_unlockAllProfitTakerStages: `Deslobquea todas las etapas del Roba-ganancias`, cheats_unlockAllProfitTakerStages: `Deslobquea todas las etapas del Roba-ganancias`,
cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`, cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`,

View File

@ -158,6 +158,7 @@ dict = {
cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`, cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`,
cheats_noDeathMarks: `Aucune marque d'assassin`, cheats_noDeathMarks: `Aucune marque d'assassin`,
cheats_noKimCooldowns: `Aucun cooldown sur le KIM`, cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`,
cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`, cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`, cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`,
cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`, cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,

View File

@ -158,6 +158,7 @@ dict = {
cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`,
cheats_noDeathMarks: `Без меток сметри`, cheats_noDeathMarks: `Без меток сметри`,
cheats_noKimCooldowns: `Чаты KIM без кулдауна`, cheats_noKimCooldowns: `Чаты KIM без кулдауна`,
cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`,
cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`, cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`, cheats_unlockAllProfitTakerStages: `[UNTRANSLATED] Unlock All Profit Taker Stages`,
cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`, cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`,

View File

@ -158,6 +158,7 @@ dict = {
cheats_noVendorPurchaseLimits: `商城或商人无购买限制`, cheats_noVendorPurchaseLimits: `商城或商人无购买限制`,
cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`, cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`,
cheats_noKimCooldowns: `无 KIM 冷却时间`, cheats_noKimCooldowns: `无 KIM 冷却时间`,
cheats_fullyStockedVendors: `[UNTRANSLATED] Fully Stocked Vendors`,
cheats_syndicateMissionsRepeatable: `集团任务可重复`, cheats_syndicateMissionsRepeatable: `集团任务可重复`,
cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`, cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`,
cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`, cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,