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

代码差异

4 个文件 +129 -293
Modified src/services/rngService.ts +11 -3
@@ -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 {
Modified src/services/serversideVendorsService.ts +117 -42
@@ -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 };
Modified src/types/vendorTypes.ts +1 -0
@@ -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.
Deleted static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json +0 -248
@@ -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 }