返回提交历史
Modified
src/services/rngService.ts
+11
-3
Modified
src/services/serversideVendorsService.ts
+117
-42
Modified
src/types/vendorTypes.ts
+1
-0
Deleted
static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json
+0
-248
XFEstudio/XFESpaceNinjaServer
feat: auto-generate debt token vendor manifest (#1827)
Yet another pretty big change to how these things are generated, but getting closer to where we wanna be now. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1827 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
506365f9
代码差异
4 个文件
+129
-293
@@ -97,9 +97,17 @@ export class CRng {
97
97
}
98
98
99
99
randomInt(min: number, max: number): number {
100
min = Math.ceil(min);
101
max = Math.floor(max);
102
return Math.floor(this.random() * (max - min + 1)) + min;
100
const diff = max - min;
101
if (diff != 0) {
102
if (diff < 0) {
103
throw new Error(`max must be greater than min`);
104
}
105
if (diff > 0x3fffffff) {
106
throw new Error(`insufficient entropy`);
107
}
108
min += Math.floor(this.random() * (diff + 1));
109
}
110
return min;
103
111
}
104
112
105
113
randomElement<T>(arr: T[]): T {
@@ -5,9 +5,10 @@ import {
5
5
IItemManifestPreprocessed,
6
6
IRawVendorManifest,
7
7
IVendorInfo,
8
IVendorInfoPreprocessed,
8
9
IVendorManifestPreprocessed
9
10
} from "@/src/types/vendorTypes";
10
import { ExportVendors } from "warframe-public-export-plus";
11
import { ExportVendors, IRange } from "warframe-public-export-plus";
11
12
12
13
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
13
14
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
@@ -32,7 +33,6 @@ import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorIn
32
33
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
33
34
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
34
35
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
35
import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json";
36
36
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
37
37
import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json";
38
38
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
@@ -63,7 +63,6 @@ const rawVendorManifests: IRawVendorManifest[] = [
63
63
OstronPetVendorManifest,
64
64
OstronProspectorVendorManifest,
65
65
RadioLegionIntermission12VendorManifest,
66
SolarisDebtTokenVendorManifest,
67
66
SolarisDebtTokenVendorRepossessionsManifest,
68
67
SolarisFishmongerVendorManifest,
69
68
SolarisProspectorVendorManifest,
@@ -72,7 +71,7 @@ const rawVendorManifests: IRawVendorManifest[] = [
72
71
];
73
72
74
73
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
75
cycleStart: number;
74
cycleOffset: number;
76
75
cycleDuration: number;
77
76
}
78
77
@@ -84,7 +83,7 @@ const generatableVendors: IGeneratableVendorInfo[] = [
84
83
RandomSeedType: "VRST_WEAPON",
85
84
RequiredGoalTag: "",
86
85
WeaponUpgradeValueAttenuationExponent: 2.25,
87
cycleStart: 1740960000_000,
86
cycleOffset: 1740960000_000,
88
87
cycleDuration: 4 * unixTimesInMs.day
89
88
},
90
89
{
@@ -93,8 +92,16 @@ const generatableVendors: IGeneratableVendorInfo[] = [
93
92
PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70",
94
93
RandomSeedType: "VRST_WEAPON",
95
94
WeaponUpgradeValueAttenuationExponent: 2.25,
96
cycleStart: 1744934400_000,
95
cycleOffset: 1744934400_000,
97
96
cycleDuration: 4 * unixTimesInMs.day
97
},
98
{
99
_id: { $oid: "5be4a159b144f3cdf1c22efa" },
100
TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest",
101
PropertyTextHash: "A39621049CA3CA13761028CD21C239EF",
102
RandomSeedType: "VRST_FLAVOUR_TEXT",
103
cycleOffset: 1734307200_000,
104
cycleDuration: unixTimesInMs.hour
98
105
}
99
106
// {
100
107
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
@@ -166,60 +173,128 @@ const refreshExpiry = (expiry: IMongoDate): number => {
166
173
return 0;
167
174
};
168
175
176
const toRange = (value: IRange | number): IRange => {
177
if (typeof value == "number") {
178
return { minValue: value, maxValue: value };
179
}
180
return value;
181
};
182
183
const vendorInfoCache: Record<string, IVendorInfoPreprocessed> = {};
184
169
185
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => {
170
const EPOCH = vendorInfo.cycleStart;
171
const manifest = ExportVendors[vendorInfo.TypeName];
172
let binThisCycle;
173
if (manifest.isOneBinPerCycle) {
174
const cycleDuration = vendorInfo.cycleDuration; // manifest.items[0].durationHours! * 3600_000;
175
const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration);
176
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
186
if (!(vendorInfo.TypeName in vendorInfoCache)) {
187
// eslint-disable-next-line @typescript-eslint/no-unused-vars
188
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
189
vendorInfoCache[vendorInfo.TypeName] = {
190
...clientVendorInfo,
191
ItemManifest: [],
192
Expiry: { $date: { $numberLong: "0" } }
193
};
177
194
}
178
const items: IItemManifestPreprocessed[] = [];
179
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
180
for (let i = 0; i != manifest.items.length; ++i) {
181
const rawItem = manifest.items[i];
182
if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) {
183
continue;
195
const processed = vendorInfoCache[vendorInfo.TypeName];
196
if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) {
197
// Remove expired offers
198
for (let i = 0; i != processed.ItemManifest.length; ) {
199
if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) {
200
processed.ItemManifest.splice(i, 1);
201
} else {
202
++i;
203
}
184
204
}
185
const cycleDuration = vendorInfo.cycleDuration; // rawItem.durationHours! * 3600_000;
186
const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration);
187
const cycleStart = EPOCH + cycleIndex * cycleDuration;
188
const cycleEnd = cycleStart + cycleDuration;
189
if (soonestOfferExpiry > cycleEnd) {
190
soonestOfferExpiry = cycleEnd;
205
206
// Add new offers
207
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
208
const cycleIndex = Math.trunc((Date.now() - vendorInfo.cycleOffset) / vendorInfo.cycleDuration);
209
const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
210
const manifest = ExportVendors[vendorInfo.TypeName];
211
const offersToAdd = [];
212
if (manifest.numItems && manifest.numItems.minValue != manifest.numItems.maxValue) {
213
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
214
while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) {
215
// TODO: Consider per-bin item limits
216
// TODO: Consider item probability weightings
217
offersToAdd.push(rng.randomElement(manifest.items));
218
}
219
} else {
220
let binThisCycle;
221
if (manifest.isOneBinPerCycle) {
222
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
223
}
224
for (const rawItem of manifest.items) {
225
if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
226
offersToAdd.push(rawItem);
227
}
228
}
191
229
}
192
const rng = new CRng(cycleIndex);
193
rng.churnSeed(i);
194
/*for (let j = -1; j != rawItem.duplicates; ++j)*/ {
230
const cycleStart = vendorInfo.cycleOffset + cycleIndex * vendorInfo.cycleDuration;
231
for (const rawItem of offersToAdd) {
232
const durationHoursRange = toRange(rawItem.durationHours);
233
const expiry =
234
cycleStart +
235
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
195
236
const item: IItemManifestPreprocessed = {
196
237
StoreItem: rawItem.storeItem,
197
ItemPrices: rawItem.itemPrices!.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
238
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
198
239
Bin: "BIN_" + rawItem.bin,
199
240
QuantityMultiplier: 1,
200
Expiry: { $date: { $numberLong: cycleEnd.toString() } },
241
Expiry: { $date: { $numberLong: expiry.toString() } },
201
242
AllowMultipurchase: false,
202
243
Id: {
203
244
$oid:
204
i.toString(16).padStart(8, "0") +
245
Math.trunc(cycleStart / 1000)
246
.toString(16)
247
.padStart(8, "0") +
205
248
vendorInfo._id.$oid.substring(8, 16) +
206
rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0")
249
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
250
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
207
251
}
208
252
};
253
if (rawItem.numRandomItemPrices) {
254
item.ItemPrices = [];
255
for (let i = 0; i != rawItem.numRandomItemPrices; ++i) {
256
let itemPrice: { type: string; count: IRange };
257
do {
258
itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin]);
259
} while (item.ItemPrices.find(x => x.ItemType == itemPrice.type));
260
item.ItemPrices.push({
261
ItemType: itemPrice.type,
262
ItemCount: rng.randomInt(itemPrice.count.minValue, itemPrice.count.maxValue),
263
ProductCategory: "MiscItems"
264
});
265
}
266
}
267
if (rawItem.credits) {
268
const value =
269
typeof rawItem.credits == "number"
270
? rawItem.credits
271
: rng.randomInt(
272
rawItem.credits.minValue / rawItem.credits.step,
273
rawItem.credits.maxValue / rawItem.credits.step
274
) * rawItem.credits.step;
275
item.RegularPrice = [value, value];
276
}
209
277
if (vendorInfo.RandomSeedType) {
210
item.LocTagRandSeed =
211
(BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff));
278
item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
279
if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
280
const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
281
item.LocTagRandSeed = (BigInt(highDword) << 32n) | BigInt(item.LocTagRandSeed);
282
}
212
283
}
213
items.push(item);
284
processed.ItemManifest.push(item);
214
285
}
286
287
// Update vendor expiry
288
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
289
for (const offer of processed.ItemManifest) {
290
const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
291
if (soonestOfferExpiry > offerExpiry) {
292
soonestOfferExpiry = offerExpiry;
293
}
294
}
295
processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
215
296
}
216
// eslint-disable-next-line @typescript-eslint/no-unused-vars
217
const { cycleStart, cycleDuration, ...clientVendorInfo } = vendorInfo;
218
297
return {
219
VendorInfo: {
220
...clientVendorInfo,
221
ItemManifest: items,
222
Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } }
223
}
298
VendorInfo: processed
224
299
};
225
300
};
@@ -13,6 +13,7 @@ export interface IItemPricePreprocessed extends Omit<IItemPrice, "ItemType"> {
13
13
export interface IItemManifest {
14
14
StoreItem: string;
15
15
ItemPrices?: IItemPrice[];
16
RegularPrice?: number[];
16
17
Bin: string;
17
18
QuantityMultiplier: number;
18
19
Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
@@ -1,248 +0,0 @@
1
{
2
"VendorInfo": {
3
"_id": {
4
"$oid": "5be4a159b144f3cdf1c22efa"
5
},
6
"TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest",
7
"ItemManifest": [
8
{
9
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD",
10
"ItemPrices": [
11
{
12
"ItemType": "/Lotus/Types/Gameplay/Venus/Resources/VenusCoconutItem",
13
"ItemCount": 5,
14
"ProductCategory": "MiscItems"
15
},
16
{
17
"ItemType": "/Lotus/Types/Items/MiscItems/Circuits",
18
"ItemCount": 3664,
19
"ProductCategory": "MiscItems"
20
}
21
],
22
"RegularPrice": [87300, 87300],
23
"Bin": "BIN_1",
24
"QuantityMultiplier": 1,
25
"Expiry": {
26
"$date": {
27
"$numberLong": "9999999000000"
28
}
29
},
30
"PurchaseQuantityLimit": 1,
31
"AllowMultipurchase": true,
32
"LocTagRandSeed": 1881404827,
33
"Id": {
34
"$oid": "670daf92d21f34757a5e73b4"
35
}
36
},
37
{
38
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleRareC",
39
"ItemPrices": [
40
{
41
"ItemType": "/Lotus/Types/Items/MiscItems/NeuralSensor",
42
"ItemCount": 1,
43
"ProductCategory": "MiscItems"
44
}
45
],
46
"RegularPrice": [53300, 53300],
47
"Bin": "BIN_2",
48
"QuantityMultiplier": 1,
49
"Expiry": {
50
"$date": {
51
"$numberLong": "9999999000000"
52
}
53
},
54
"PurchaseQuantityLimit": 1,
55
"AllowMultipurchase": true,
56
"LocTagRandSeed": 1943984533,
57
"Id": {
58
"$oid": "6710b5029e1a3080a65e73a7"
59
}
60
},
61
{
62
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonG",
63
"ItemPrices": [
64
{
65
"ItemType": "/Lotus/Types/Items/MiscItems/Salvage",
66
"ItemCount": 11540,
67
"ProductCategory": "MiscItems"
68
}
69
],
70
"RegularPrice": [27300, 27300],
71
"Bin": "BIN_0",
72
"QuantityMultiplier": 1,
73
"Expiry": {
74
"$date": {
75
"$numberLong": "9999999000000"
76
}
77
},
78
"PurchaseQuantityLimit": 1,
79
"AllowMultipurchase": true,
80
"LocTagRandSeed": 744199559,
81
"Id": {
82
"$oid": "67112582cc115756985e73a4"
83
}
84
},
85
{
86
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB",
87
"ItemPrices": [
88
{
89
"ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem",
90
"ItemCount": 9,
91
"ProductCategory": "MiscItems"
92
}
93
],
94
"RegularPrice": [75800, 75800],
95
"Bin": "BIN_1",
96
"QuantityMultiplier": 1,
97
"Expiry": {
98
"$date": {
99
"$numberLong": "9999999000000"
100
}
101
},
102
"PurchaseQuantityLimit": 1,
103
"AllowMultipurchase": true,
104
"LocTagRandSeed": 3744711432,
105
"Id": {
106
"$oid": "670de7d28a6ec82cd25e73a2"
107
}
108
},
109
{
110
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB",
111
"ItemPrices": [
112
{
113
"ItemType": "/Lotus/Types/Items/MiscItems/Rubedo",
114
"ItemCount": 3343,
115
"ProductCategory": "MiscItems"
116
}
117
],
118
"RegularPrice": [52200, 52200],
119
"Bin": "BIN_1",
120
"QuantityMultiplier": 1,
121
"Expiry": {
122
"$date": {
123
"$numberLong": "9999999000000"
124
}
125
},
126
"PurchaseQuantityLimit": 1,
127
"AllowMultipurchase": true,
128
"LocTagRandSeed": 1579000687,
129
"Id": {
130
"$oid": "670e58526171148e125e73ad"
131
}
132
},
133
{
134
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonA",
135
"ItemPrices": [
136
{
137
"ItemType": "/Lotus/Types/Gameplay/Venus/Resources/CoolantItem",
138
"ItemCount": 9,
139
"ProductCategory": "MiscItems"
140
},
141
{
142
"ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem",
143
"ItemCount": 5,
144
"ProductCategory": "MiscItems"
145
}
146
],
147
"RegularPrice": [12400, 12400],
148
"Bin": "BIN_0",
149
"QuantityMultiplier": 1,
150
"Expiry": {
151
"$date": {
152
"$numberLong": "9999999000000"
153
}
154
},
155
"PurchaseQuantityLimit": 1,
156
"AllowMultipurchase": true,
157
"LocTagRandSeed": 3589081466,
158
"Id": {
159
"$oid": "67112582cc115756985e73a5"
160
}
161
},
162
{
163
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonC",
164
"ItemPrices": [
165
{
166
"ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonOreBAlloyItem",
167
"ItemCount": 13,
168
"ProductCategory": "MiscItems"
169
}
170
],
171
"RegularPrice": [77500, 77500],
172
"Bin": "BIN_1",
173
"QuantityMultiplier": 1,
174
"Expiry": {
175
"$date": {
176
"$numberLong": "9999999000000"
177
}
178
},
179
"PurchaseQuantityLimit": 1,
180
"AllowMultipurchase": true,
181
"LocTagRandSeed": 1510234814,
182
"Id": {
183
"$oid": "670f0f21250ad046c35e73ee"
184
}
185
},
186
{
187
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD",
188
"ItemPrices": [
189
{
190
"ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishParralelBiodeItem",
191
"ItemCount": 7,
192
"ProductCategory": "MiscItems"
193
},
194
{
195
"ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonGemBCutItem",
196
"ItemCount": 12,
197
"ProductCategory": "MiscItems"
198
}
199
],
200
"RegularPrice": [94600, 94600],
201
"Bin": "BIN_1",
202
"QuantityMultiplier": 1,
203
"Expiry": {
204
"$date": {
205
"$numberLong": "9999999000000"
206
}
207
},
208
"PurchaseQuantityLimit": 1,
209
"AllowMultipurchase": true,
210
"LocTagRandSeed": 4222095721,
211
"Id": {
212
"$oid": "670f63827be40254f95e739d"
213
}
214
},
215
{
216
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonJ",
217
"ItemPrices": [
218
{
219
"ItemType": "/Lotus/Types/Items/MiscItems/Nanospores",
220
"ItemCount": 14830,
221
"ProductCategory": "MiscItems"
222
}
223
],
224
"RegularPrice": [25600, 25600],
225
"Bin": "BIN_0",
226
"QuantityMultiplier": 1,
227
"Expiry": {
228
"$date": {
229
"$numberLong": "9999999000000"
230
}
231
},
232
"PurchaseQuantityLimit": 1,
233
"AllowMultipurchase": true,
234
"LocTagRandSeed": 2694388669,
235
"Id": {
236
"$oid": "67112582cc115756985e73a6"
237
}
238
}
239
],
240
"PropertyTextHash": "A39621049CA3CA13761028CD21C239EF",
241
"RandomSeedType": "VRST_FLAVOUR_TEXT",
242
"Expiry": {
243
"$date": {
244
"$numberLong": "9999999000000"
245
}
246
}
247
}
248
}