返回提交历史
Modified
src/controllers/api/getVendorInfoController.ts
+2
-2
Modified
src/services/leaderboardService.ts
+1
-2
Modified
src/services/purchaseService.ts
+20
-7
Modified
src/services/serversideVendorsService.ts
+41
-23
Added
src/types/vendorTypes.ts
+46
-0
Modified
static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json
+41
-11
Modified
static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json
+6
-6
Modified
static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json
+3
-3
XFEstudio/XFESpaceNinjaServer
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
代码差异
8 个文件
+160
-54
@@ -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
}
@@ -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);
@@ -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
};
@@ -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
};
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}