XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

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
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

4 个文件 +78 -676
Modified package-lock.json +4 -4
@@ -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",
Modified package.json +1 -1
@@ -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"
Modified src/services/serversideVendorsService.ts +73 -68
@@ -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 (