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: initial vendor rotations (#1360)

Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1360 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

6a1e5081
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

8 个文件 +160 -54
Modified src/controllers/api/getVendorInfoController.ts +2 -2
@@ -1,5 +1,5 @@
1 1 import { RequestHandler } from "express";
2 import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
2 import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService";
3 3
4 4 export const getVendorInfoController: RequestHandler = (req, res) => {
5 5 if (typeof req.query.vendor == "string") {
@@ -7,7 +7,7 @@ export const getVendorInfoController: RequestHandler = (req, res) => {
7 7 if (!manifest) {
8 8 throw new Error(`Unknown vendor: ${req.query.vendor}`);
9 9 }
10 res.json(manifest);
10 res.json(preprocessVendorManifest(manifest));
11 11 } else {
12 12 res.status(400).end();
13 13 }
Modified src/services/leaderboardService.ts +1 -2
@@ -15,8 +15,7 @@ export const submitLeaderboardScore = async (
15 15 expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000);
16 16 } else {
17 17 const EPOCH = 1734307200 * 1000; // Monday
18 const day = Math.trunc((Date.now() - EPOCH) / 86400000);
19 const week = Math.trunc(day / 7);
18 const week = Math.trunc((Date.now() - EPOCH) / 604800000);
20 19 const weekStart = EPOCH + week * 604800000;
21 20 const weekEnd = weekStart + 604800000;
22 21 expiry = new Date(weekEnd);
Modified src/services/purchaseService.ts +20 -7
@@ -9,7 +9,7 @@ import {
9 9 updateSlots
10 10 } from "@/src/services/inventoryService";
11 11 import { getRandomWeightedRewardUc } from "@/src/services/rngService";
12 import { getVendorManifestByOid } from "@/src/services/serversideVendorsService";
12 import { getVendorManifestByOid, preprocessVendorManifest } from "@/src/services/serversideVendorsService";
13 13 import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
14 14 import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
15 15 import { logger } from "@/src/utils/logger";
@@ -52,8 +52,9 @@ export const handlePurchase = async (
52 52
53 53 const prePurchaseInventoryChanges: IInventoryChanges = {};
54 54 if (purchaseRequest.PurchaseParams.Source == 7) {
55 const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
56 if (manifest) {
55 const rawManifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
56 if (rawManifest) {
57 const manifest = preprocessVendorManifest(rawManifest);
57 58 let ItemId: string | undefined;
58 59 if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
59 60 ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
@@ -87,16 +88,28 @@ export const handlePurchase = async (
87 88 }) - 1
88 89 ];
89 90 }
91 let expiry = parseInt(offer.Expiry.$date.$numberLong);
92 if (purchaseRequest.PurchaseParams.IsWeekly) {
93 const EPOCH = 1734307200 * 1000; // Monday
94 const week = Math.trunc((Date.now() - EPOCH) / 604800000);
95 const weekStart = EPOCH + week * 604800000;
96 expiry = weekStart + 604800000;
97 }
90 98 const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
91 99 let numPurchased = purchaseRequest.PurchaseParams.Quantity;
92 100 if (historyEntry) {
93 numPurchased += historyEntry.NumPurchased;
94 historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity;
101 if (Date.now() >= historyEntry.Expiry.getTime()) {
102 historyEntry.NumPurchased = numPurchased;
103 historyEntry.Expiry = new Date(expiry);
104 } else {
105 numPurchased += historyEntry.NumPurchased;
106 historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity;
107 }
95 108 } else {
96 109 vendorPurchases.PurchaseHistory.push({
97 110 ItemId: ItemId,
98 111 NumPurchased: purchaseRequest.PurchaseParams.Quantity,
99 Expiry: new Date(parseInt(offer.Expiry.$date.$numberLong))
112 Expiry: new Date(expiry)
100 113 });
101 114 }
102 115 prePurchaseInventoryChanges.NewVendorPurchase = {
@@ -105,7 +118,7 @@ export const handlePurchase = async (
105 118 {
106 119 ItemId: ItemId,
107 120 NumPurchased: numPurchased,
108 Expiry: offer.Expiry
121 Expiry: { $date: { $numberLong: expiry.toString() } }
109 122 }
110 123 ]
111 124 };
Modified src/services/serversideVendorsService.ts +41 -23
@@ -1,4 +1,6 @@
1 import { IMongoDate, IOid } from "@/src/types/commonTypes";
1 import { CRng, mixSeeds } from "@/src/services/rngService";
2 import { IMongoDate } from "@/src/types/commonTypes";
3 import { IVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes";
2 4
3 5 import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
4 6 import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
@@ -31,25 +33,6 @@ import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorI
31 33 import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
32 34 import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
33 35
34 interface IVendorManifest {
35 VendorInfo: {
36 _id: IOid;
37 TypeName: string;
38 ItemManifest: {
39 StoreItem: string;
40 ItemPrices?: { ItemType: string; ItemCount: number; ProductCategory: string }[];
41 Bin: string;
42 QuantityMultiplier: number;
43 Expiry: IMongoDate;
44 PurchaseQuantityLimit?: number;
45 RotatedWeekly?: boolean;
46 AllowMultipurchase: boolean;
47 Id: IOid;
48 }[];
49 Expiry: IMongoDate;
50 };
51 }
52
53 36 const vendorManifests: IVendorManifest[] = [
54 37 ArchimedeanVendorManifest,
55 38 DeimosEntratiFragmentVendorProductsManifest,
@@ -65,8 +48,8 @@ const vendorManifests: IVendorManifest[] = [
65 48 DuviriAcrithisVendorManifest,
66 49 EntratiLabsEntratiLabsCommisionsManifest,
67 50 EntratiLabsEntratiLabVendorManifest,
68 GuildAdvertisementVendorManifest,
69 HubsIronwakeDondaVendorManifest,
51 GuildAdvertisementVendorManifest, // uses preprocessing
52 HubsIronwakeDondaVendorManifest, // uses preprocessing
70 53 HubsPerrinSequenceWeaponVendorManifest,
71 54 HubsRailjackCrewMemberVendorManifest,
72 55 MaskSalesmanManifest,
@@ -79,7 +62,7 @@ const vendorManifests: IVendorManifest[] = [
79 62 SolarisDebtTokenVendorRepossessionsManifest,
80 63 SolarisFishmongerVendorManifest,
81 64 SolarisProspectorVendorManifest,
82 TeshinHardModeVendorManifest,
65 TeshinHardModeVendorManifest, // uses preprocessing
83 66 ZarimanCommisionsManifestArchimedean
84 67 ];
85 68
@@ -100,3 +83,38 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined
100 83 }
101 84 return undefined;
102 85 };
86
87 export const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifestPreprocessed => {
88 if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
89 const manifest = structuredClone(originalManifest);
90 const info = manifest.VendorInfo;
91 refreshExpiry(info.Expiry);
92 for (const offer of info.ItemManifest) {
93 const iteration = refreshExpiry(offer.Expiry);
94 if (offer.ItemPrices) {
95 for (const price of offer.ItemPrices) {
96 if (typeof price.ItemType != "string") {
97 const itemSeed = parseInt(offer.Id.$oid.substring(16), 16);
98 const rng = new CRng(mixSeeds(itemSeed, iteration));
99 price.ItemType = rng.randomElement(price.ItemType);
100 }
101 }
102 }
103 }
104 return manifest as IVendorManifestPreprocessed;
105 }
106 return originalManifest as IVendorManifestPreprocessed;
107 };
108
109 const refreshExpiry = (expiry: IMongoDate): number => {
110 const period = parseInt(expiry.$date.$numberLong);
111 if (Date.now() >= period) {
112 const epoch = 1734307200 * 1000; // Monday (for weekly schedules)
113 const iteration = Math.trunc((Date.now() - epoch) / period);
114 const start = epoch + iteration * period;
115 const end = start + period;
116 expiry.$date.$numberLong = end.toString();
117 return iteration;
118 }
119 return 0;
120 };
Added src/types/vendorTypes.ts +46 -0
@@ -0,0 +1,46 @@
1 import { IMongoDate, IOid } from "./commonTypes";
2
3 interface IItemPrice {
4 ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period.
5 ItemCount: number;
6 ProductCategory: string;
7 }
8
9 interface IItemPricePreprocessed extends Omit<IItemPrice, "ItemType"> {
10 ItemType: string;
11 }
12
13 interface IItemManifest {
14 StoreItem: string;
15 ItemPrices?: IItemPrice[];
16 Bin: string;
17 QuantityMultiplier: number;
18 Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
19 PurchaseQuantityLimit?: number;
20 RotatedWeekly?: boolean;
21 AllowMultipurchase: boolean;
22 Id: IOid;
23 }
24
25 interface IItemManifestPreprocessed extends Omit<IItemManifest, "ItemPrices"> {
26 ItemPrices?: IItemPricePreprocessed[];
27 }
28
29 interface IVendorInfo {
30 _id: IOid;
31 TypeName: string;
32 ItemManifest: IItemManifest[];
33 Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
34 }
35
36 interface IVendorInfoPreprocessed extends Omit<IVendorInfo, "ItemManifest"> {
37 ItemManifest: IItemManifestPreprocessed[];
38 }
39
40 export interface IVendorManifest {
41 VendorInfo: IVendorInfo;
42 }
43
44 export interface IVendorManifestPreprocessed {
45 VendorInfo: IVendorInfoPreprocessed;
46 }
Modified static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json +41 -11
@@ -5,11 +5,17 @@
5 5 "ItemManifest": [
6 6 {
7 7 "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon",
8 "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 12, "ProductCategory": "MiscItems" }],
8 "ItemPrices": [
9 {
10 "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
11 "ItemCount": 12,
12 "ProductCategory": "MiscItems"
13 }
14 ],
9 15 "RegularPrice": [1, 1],
10 16 "Bin": "BIN_4",
11 17 "QuantityMultiplier": 1,
12 "Expiry": { "$date": { "$numberLong": "9999999000000" } },
18 "Expiry": { "$date": { "$numberLong": "604800000" } },
13 19 "PurchaseQuantityLimit": 1,
14 20 "AllowMultipurchase": false,
15 21 "LocTagRandSeed": 79554843,
@@ -17,11 +23,17 @@
17 23 },
18 24 {
19 25 "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain",
20 "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 7, "ProductCategory": "MiscItems" }],
26 "ItemPrices": [
27 {
28 "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
29 "ItemCount": 7,
30 "ProductCategory": "MiscItems"
31 }
32 ],
21 33 "RegularPrice": [1, 1],
22 34 "Bin": "BIN_3",
23 35 "QuantityMultiplier": 1,
24 "Expiry": { "$date": { "$numberLong": "9999999000000" } },
36 "Expiry": { "$date": { "$numberLong": "604800000" } },
25 37 "PurchaseQuantityLimit": 1,
26 38 "AllowMultipurchase": false,
27 39 "LocTagRandSeed": 2413820225,
@@ -29,11 +41,17 @@
29 41 },
30 42 {
31 43 "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm",
32 "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 3, "ProductCategory": "MiscItems" }],
44 "ItemPrices": [
45 {
46 "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
47 "ItemCount": 3,
48 "ProductCategory": "MiscItems"
49 }
50 ],
33 51 "RegularPrice": [1, 1],
34 52 "Bin": "BIN_2",
35 53 "QuantityMultiplier": 1,
36 "Expiry": { "$date": { "$numberLong": "9999999000000" } },
54 "Expiry": { "$date": { "$numberLong": "604800000" } },
37 55 "PurchaseQuantityLimit": 1,
38 56 "AllowMultipurchase": false,
39 57 "LocTagRandSeed": 3262300883,
@@ -41,11 +59,17 @@
41 59 },
42 60 {
43 61 "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow",
44 "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 20, "ProductCategory": "MiscItems" }],
62 "ItemPrices": [
63 {
64 "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
65 "ItemCount": 20,
66 "ProductCategory": "MiscItems"
67 }
68 ],
45 69 "RegularPrice": [1, 1],
46 70 "Bin": "BIN_1",
47 71 "QuantityMultiplier": 1,
48 "Expiry": { "$date": { "$numberLong": "9999999000000" } },
72 "Expiry": { "$date": { "$numberLong": "604800000" } },
49 73 "PurchaseQuantityLimit": 1,
50 74 "AllowMultipurchase": false,
51 75 "LocTagRandSeed": 2797325750,
@@ -53,11 +77,17 @@
53 77 },
54 78 {
55 79 "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost",
56 "ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 10, "ProductCategory": "MiscItems" }],
80 "ItemPrices": [
81 {
82 "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
83 "ItemCount": 10,
84 "ProductCategory": "MiscItems"
85 }
86 ],
57 87 "RegularPrice": [1, 1],
58 88 "Bin": "BIN_0",
59 89 "QuantityMultiplier": 1,
60 "Expiry": { "$date": { "$numberLong": "9999999000000" } },
90 "Expiry": { "$date": { "$numberLong": "604800000" } },
61 91 "PurchaseQuantityLimit": 1,
62 92 "AllowMultipurchase": false,
63 93 "LocTagRandSeed": 554932310,
@@ -66,6 +96,6 @@
66 96 ],
67 97 "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA",
68 98 "RandomSeedType": "VRST_FLAVOUR_TEXT",
69 "Expiry": { "$date": { "$numberLong": "9999999000000" } }
99 "Expiry": { "$date": { "$numberLong": "604800000" } }
70 100 }
71 101 }
Modified static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json +6 -6
@@ -18,7 +18,7 @@
18 18 "QuantityMultiplier": 1,
19 19 "Expiry": {
20 20 "$date": {
21 "$numberLong": "9999999000000"
21 "$numberLong": "604800000"
22 22 }
23 23 },
24 24 "AllowMultipurchase": true,
@@ -39,7 +39,7 @@
39 39 "QuantityMultiplier": 1,
40 40 "Expiry": {
41 41 "$date": {
42 "$numberLong": "9999999000000"
42 "$numberLong": "604800000"
43 43 }
44 44 },
45 45 "PurchaseQuantityLimit": 1,
@@ -61,7 +61,7 @@
61 61 "QuantityMultiplier": 1,
62 62 "Expiry": {
63 63 "$date": {
64 "$numberLong": "9999999000000"
64 "$numberLong": "604800000"
65 65 }
66 66 },
67 67 "PurchaseQuantityLimit": 1,
@@ -83,7 +83,7 @@
83 83 "QuantityMultiplier": 35000,
84 84 "Expiry": {
85 85 "$date": {
86 "$numberLong": "9999999000000"
86 "$numberLong": "604800000"
87 87 }
88 88 },
89 89 "PurchaseQuantityLimit": 1,
@@ -105,7 +105,7 @@
105 105 "QuantityMultiplier": 1,
106 106 "Expiry": {
107 107 "$date": {
108 "$numberLong": "9999999000000"
108 "$numberLong": "604800000"
109 109 }
110 110 },
111 111 "PurchaseQuantityLimit": 1,
@@ -118,7 +118,7 @@
118 118 "PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621",
119 119 "Expiry": {
120 120 "$date": {
121 "$numberLong": "9999999000000"
121 "$numberLong": "604800000"
122 122 }
123 123 }
124 124 }
Modified static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json +3 -3
@@ -561,7 +561,7 @@
561 561 "QuantityMultiplier": 1,
562 562 "Expiry": {
563 563 "$date": {
564 "$numberLong": "2051240400000"
564 "$numberLong": "604800000"
565 565 }
566 566 },
567 567 "PurchaseQuantityLimit": 25,
@@ -583,7 +583,7 @@
583 583 "QuantityMultiplier": 10000,
584 584 "Expiry": {
585 585 "$date": {
586 "$numberLong": "2051240400000"
586 "$numberLong": "604800000"
587 587 }
588 588 },
589 589 "PurchaseQuantityLimit": 25,
@@ -596,7 +596,7 @@
596 596 "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D",
597 597 "Expiry": {
598 598 "$date": {
599 "$numberLong": "2051240400000"
599 "$numberLong": "604800000"
600 600 }
601 601 }
602 602 }