返回提交历史
Modified
package-lock.json
+4
-4
Modified
package.json
+1
-1
Modified
src/services/serversideVendorsService.ts
+73
-68
Deleted
static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json
+0
-603
XFEstudio/SpaceNinjaServer
feat: autogenerate steel path honors vendor (#2187)
No more "preprocessing" needed now. Some good progress for #1225, I'd say. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2187 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
6691d4e4
代码差异
4 个文件
+78
-676
@@ -18,7 +18,7 @@
18
18
"morgan": "^1.10.0",
19
19
"ncp": "^2.0.0",
20
20
"typescript": "^5.5",
21
"warframe-public-export-plus": "^0.5.67",
21
"warframe-public-export-plus": "^0.5.68",
22
22
"warframe-riven-info": "^0.1.2",
23
23
"winston": "^3.17.0",
24
24
"winston-daily-rotate-file": "^5.0.0"
@@ -3814,9 +3814,9 @@
3814
3814
}
3815
3815
},
3816
3816
"node_modules/warframe-public-export-plus": {
3817
"version": "0.5.67",
3818
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.67.tgz",
3819
"integrity": "sha512-LsnZD2E5PTA+5MK9kDGvM/hFDtg8sb0EwQ4hKH5ILqrSgz30a9W8785v77RSsL1AEVF8dfb/lZcSTCJq1DZHzQ=="
3817
"version": "0.5.68",
3818
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.68.tgz",
3819
"integrity": "sha512-KMmwCVeQ4k+EN73UZqxnM+qQdPsST8geWoJCP7US5LT6JcRxa8ptmqYXwCzaLtckBLZyVbamsxKZAxPPJckxsA=="
3820
3820
},
3821
3821
"node_modules/warframe-riven-info": {
3822
3822
"version": "0.1.2",
@@ -25,7 +25,7 @@
25
25
"morgan": "^1.10.0",
26
26
"ncp": "^2.0.0",
27
27
"typescript": "^5.5",
28
"warframe-public-export-plus": "^0.5.67",
28
"warframe-public-export-plus": "^0.5.68",
29
29
"warframe-riven-info": "^0.1.2",
30
30
"winston": "^3.17.0",
31
31
"winston-daily-rotate-file": "^5.0.0"
@@ -3,7 +3,6 @@ import { isDev } from "@/src/helpers/pathHelper";
3
3
import { catBreadHash } from "@/src/helpers/stringHelpers";
4
4
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
5
5
import { mixSeeds, SRng } from "@/src/services/rngService";
6
import { IMongoDate } from "@/src/types/commonTypes";
7
6
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
8
7
import { logger } from "@/src/utils/logger";
9
8
import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus";
@@ -25,7 +24,6 @@ import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo
25
24
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
26
25
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
27
26
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
28
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
29
27
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
30
28
31
29
const rawVendorManifests: IVendorManifest[] = [
@@ -46,7 +44,6 @@ const rawVendorManifests: IVendorManifest[] = [
46
44
OstronPetVendorManifest,
47
45
SolarisDebtTokenVendorRepossessionsManifest,
48
46
Temple1999VendorManifest,
49
TeshinHardModeVendorManifest, // uses preprocessing
50
47
ZarimanCommisionsManifestArchimedean
51
48
];
52
49
@@ -87,12 +84,16 @@ const gcd = (a: number, b: number): number => {
87
84
const getCycleDuration = (manifest: IVendor): number => {
88
85
let dur = 0;
89
86
for (const item of manifest.items) {
90
if (typeof item.durationHours != "number") {
87
if (item.alwaysOffered) {
88
continue;
89
}
90
const durationHours = item.rotatedWeekly ? 168 : item.durationHours;
91
if (typeof durationHours != "number") {
91
92
dur = 1;
92
93
break;
93
94
}
94
if (dur != item.durationHours) {
95
dur = gcd(dur, item.durationHours);
95
if (dur != durationHours) {
96
dur = gcd(dur, durationHours);
96
97
}
97
98
}
98
99
return dur * unixTimesInMs.hour;
@@ -101,7 +102,7 @@ const getCycleDuration = (manifest: IVendor): number => {
101
102
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
102
103
for (const vendorManifest of rawVendorManifests) {
103
104
if (vendorManifest.VendorInfo.TypeName == typeName) {
104
return preprocessVendorManifest(vendorManifest);
105
return vendorManifest;
105
106
}
106
107
}
107
108
for (const vendorInfo of generatableVendors) {
@@ -124,7 +125,7 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest |
124
125
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
125
126
for (const vendorManifest of rawVendorManifests) {
126
127
if (vendorManifest.VendorInfo._id.$oid == oid) {
127
return preprocessVendorManifest(vendorManifest);
128
return vendorManifest;
128
129
}
129
130
}
130
131
for (const vendorInfo of generatableVendors) {
@@ -183,30 +184,6 @@ export const applyStandingToVendorManifest = (
183
184
};
184
185
};
185
186
186
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
187
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
188
const manifest = structuredClone(originalManifest);
189
const info = manifest.VendorInfo;
190
refreshExpiry(info.Expiry);
191
for (const offer of info.ItemManifest) {
192
refreshExpiry(offer.Expiry);
193
}
194
return manifest;
195
}
196
return originalManifest;
197
};
198
199
const refreshExpiry = (expiry: IMongoDate): void => {
200
const period = parseInt(expiry.$date.$numberLong);
201
if (Date.now() >= period) {
202
const epoch = 1734307200_000; // Monday (for weekly schedules)
203
const iteration = Math.trunc((Date.now() - epoch) / period);
204
const start = epoch + iteration * period;
205
const end = start + period;
206
expiry.$date.$numberLong = end.toString();
207
}
208
};
209
210
187
const toRange = (value: IRange | number): IRange => {
211
188
if (typeof value == "number") {
212
189
return { minValue: value, maxValue: value };
@@ -230,6 +207,18 @@ const getCycleDurationRange = (manifest: IVendor): IRange | undefined => {
230
207
return res.maxValue != 0 ? res : undefined;
231
208
};
232
209
210
type TOfferId = string;
211
212
const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => {
213
if ("storeItem" in offer) {
214
// IVendorOffer
215
return offer.storeItem + "x" + offer.quantity;
216
} else {
217
// IItemManifest
218
return offer.StoreItem + "x" + offer.QuantityMultiplier;
219
}
220
};
221
233
222
const vendorManifestCache: Record<string, IVendorManifest> = {};
234
223
235
224
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
@@ -270,7 +259,8 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
270
259
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
271
260
const offersToAdd: IVendorOffer[] = [];
272
261
if (!manifest.isOneBinPerCycle) {
273
const remainingItemCapacity: Record<string, number> = {};
262
// Compute vendor requirements, subtracting existing offers
263
const remainingItemCapacity: Record<TOfferId, number> = {};
274
264
const missingItemsPerBin: Record<number, number> = {};
275
265
let numOffersThatNeedToMatchABin = 0;
276
266
if (manifest.numItemsPerBin) {
@@ -280,56 +270,59 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
280
270
}
281
271
}
282
272
for (const item of manifest.items) {
283
remainingItemCapacity[item.storeItem] = 1 + item.duplicates;
273
remainingItemCapacity[getOfferId(item)] = 1 + item.duplicates;
284
274
}
285
275
for (const offer of info.ItemManifest) {
286
remainingItemCapacity[offer.StoreItem] -= 1;
276
remainingItemCapacity[getOfferId(offer)] -= 1;
287
277
const bin = parseInt(offer.Bin.substring(4));
288
278
if (missingItemsPerBin[bin]) {
289
279
missingItemsPerBin[bin] -= 1;
290
280
numOffersThatNeedToMatchABin -= 1;
291
281
}
292
282
}
293
if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) {
294
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
283
284
// Add permanent offers
285
let numUncountedOffers = 0;
286
let offset = 0;
287
for (const item of manifest.items) {
288
if (item.alwaysOffered || item.rotatedWeekly) {
289
++numUncountedOffers;
290
const id = getOfferId(item);
291
if (remainingItemCapacity[id] != 0) {
292
remainingItemCapacity[id] -= 1;
293
offersToAdd.push(item);
294
++offset;
295
}
296
}
297
}
298
299
// Add counted offers
300
if (manifest.numItems) {
301
const useRng = manifest.numItems.minValue != manifest.numItems.maxValue;
302
const numItemsTarget =
303
numUncountedOffers +
304
(useRng
305
? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue)
306
: manifest.numItems.minValue);
307
let i = 0;
295
308
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
296
// TODO: Consider item probability weightings
297
const item = rng.randomElement(manifest.items)!;
309
const item = useRng ? rng.randomElement(manifest.items)! : manifest.items[i++];
298
310
if (
299
remainingItemCapacity[item.storeItem] != 0 &&
311
!item.alwaysOffered &&
312
remainingItemCapacity[getOfferId(item)] != 0 &&
300
313
(numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin])
301
314
) {
302
remainingItemCapacity[item.storeItem] -= 1;
315
remainingItemCapacity[getOfferId(item)] -= 1;
303
316
if (missingItemsPerBin[item.bin]) {
304
317
missingItemsPerBin[item.bin] -= 1;
305
318
numOffersThatNeedToMatchABin -= 1;
306
319
}
307
offersToAdd.push(item);
320
offersToAdd.splice(offset, 0, item);
308
321
}
309
}
310
} else {
311
for (const item of manifest.items) {
312
if (!item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
313
remainingItemCapacity[item.storeItem] -= 1;
314
offersToAdd.push(item);
322
if (i == manifest.items.length) {
323
i = 0;
315
324
}
316
325
}
317
for (const e of Object.entries(remainingItemCapacity)) {
318
const item = manifest.items.find(x => x.storeItem == e[0])!;
319
if (!item.alwaysOffered) {
320
while (e[1] != 0) {
321
e[1] -= 1;
322
offersToAdd.push(item);
323
}
324
}
325
}
326
for (const item of manifest.items) {
327
if (item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
328
remainingItemCapacity[item.storeItem] -= 1;
329
offersToAdd.push(item);
330
}
331
}
332
offersToAdd.reverse();
333
326
}
334
327
} else {
335
328
const binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
@@ -342,16 +335,21 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
342
335
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
343
336
for (const rawItem of offersToAdd) {
344
337
const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
345
const expiry =
346
cycleStart +
347
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
338
const expiry = rawItem.alwaysOffered
339
? 2051240400_000
340
: cycleStart +
341
(rawItem.rotatedWeekly
342
? unixTimesInMs.week
343
: rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour);
348
344
const item: IItemManifest = {
349
345
StoreItem: rawItem.storeItem,
350
346
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
351
347
Bin: "BIN_" + rawItem.bin,
352
348
QuantityMultiplier: rawItem.quantity,
353
349
Expiry: { $date: { $numberLong: expiry.toString() } },
354
AllowMultipurchase: false,
350
PurchaseQuantityLimit: rawItem.purchaseLimit,
351
RotatedWeekly: rawItem.rotatedWeekly,
352
AllowMultipurchase: rawItem.purchaseLimit !== 1,
355
353
Id: {
356
354
$oid:
357
355
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
@@ -422,6 +420,13 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
422
420
};
423
421
424
422
if (isDev) {
423
if (
424
getCycleDuration(ExportVendors["/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest"]) !=
425
unixTimesInMs.week
426
) {
427
logger.warn(`getCycleDuration self test failed`);
428
}
429
425
430
const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")!
426
431
.VendorInfo.ItemManifest;
427
432
if (