返回提交历史
Modified
src/controllers/api/modularWeaponSaleController.ts
+2
-2
Modified
src/services/loginRewardService.ts
+6
-6
Modified
src/services/rngService.ts
+3
-45
Modified
src/services/serversideVendorsService.ts
+5
-6
Modified
src/services/worldStateService.ts
+16
-16
XFEstudio/XFESpaceNinjaServer
chore: use 64-bit RNG everywhere (#2030)
Closes #2026 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2030 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
3fc2dccf
代码差异
5 个文件
+32
-75
@@ -2,7 +2,7 @@ import { RequestHandler } from "express";
2
2
import { ExportWeapons } from "warframe-public-export-plus";
3
3
import { IMongoDate } from "@/src/types/commonTypes";
4
4
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
5
import { CRng } from "@/src/services/rngService";
5
import { SRng } from "@/src/services/rngService";
6
6
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
7
7
import { getJSONfromString } from "@/src/helpers/stringHelpers";
8
8
import {
@@ -140,7 +140,7 @@ const getModularWeaponSale = (
140
140
partTypes: string[],
141
141
getItemType: (parts: string[]) => string
142
142
): IModularWeaponSaleInfo => {
143
const rng = new CRng(day);
143
const rng = new SRng(day);
144
144
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
145
145
let partsCost = 0;
146
146
for (const part of parts) {
@@ -1,7 +1,7 @@
1
1
import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json";
2
2
import { IInventoryChanges } from "../types/purchaseTypes";
3
3
import { TAccountDocument } from "./loginService";
4
import { CRng, mixSeeds } from "./rngService";
4
import { mixSeeds, SRng } from "./rngService";
5
5
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
6
6
import { addBooster, updateCurrency } from "./inventoryService";
7
7
import { handleStoreItemAcquisition } from "./purchaseService";
@@ -49,8 +49,8 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu
49
49
// Always produces the same result for the same account _id & LoginDays pair.
50
50
export const isLoginRewardAChoice = (account: TAccountDocument): boolean => {
51
51
const accountSeed = parseInt(account._id.toString().substring(16), 16);
52
const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
53
return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
52
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
53
return rng.randomFloat() < 0.25;
54
54
};
55
55
56
56
// Always produces the same result for the same account _id & LoginDays pair.
@@ -59,8 +59,8 @@ export const getRandomLoginRewards = (
59
59
inventory: TInventoryDatabaseDocument
60
60
): ILoginReward[] => {
61
61
const accountSeed = parseInt(account._id.toString().substring(16), 16);
62
const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
63
const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
62
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
63
const pick_a_door = rng.randomFloat() < 0.25;
64
64
const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)];
65
65
if (pick_a_door) {
66
66
do {
@@ -73,7 +73,7 @@ export const getRandomLoginRewards = (
73
73
return rewards;
74
74
};
75
75
76
const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
76
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
77
77
const reward = rng.randomReward(randomRewards)!;
78
78
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
79
79
if (reward.RewardType == "RT_RANDOM_RECIPE") {
@@ -86,54 +86,12 @@ export const mixSeeds = (seed1: number, seed2: number): number => {
86
86
return seed >>> 0;
87
87
};
88
88
89
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
90
export class CRng {
91
state: number;
92
93
constructor(seed: number = 1) {
94
this.state = seed;
95
}
96
97
random(): number {
98
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
99
return (this.state & 0x3fffffff) / 0x3fffffff;
100
}
101
102
randomInt(min: number, max: number): number {
103
const diff = max - min;
104
if (diff != 0) {
105
if (diff < 0) {
106
throw new Error(`max must be greater than min`);
107
}
108
if (diff > 0x3fffffff) {
109
throw new Error(`insufficient entropy`);
110
}
111
min += Math.floor(this.random() * (diff + 1));
112
}
113
return min;
114
}
115
116
randomElement<T>(arr: readonly T[]): T | undefined {
117
return arr[Math.floor(this.random() * arr.length)];
118
}
119
120
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
121
return getRewardAtPercentage(pool, this.random());
122
}
123
124
churnSeed(its: number): void {
125
while (its--) {
126
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
127
}
128
}
129
}
130
131
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
89
// Seeded RNG with identical results to the game client. Based on work by Donald Knuth.
132
90
export class SRng {
133
91
state: bigint;
134
92
135
constructor(seed: bigint) {
136
this.state = seed;
93
constructor(seed: bigint | number) {
94
this.state = BigInt(seed);
137
95
}
138
96
139
97
randomInt(min: number, max: number): number {
@@ -1,6 +1,6 @@
1
1
import { unixTimesInMs } from "@/src/constants/timeConstants";
2
2
import { catBreadHash } from "@/src/helpers/stringHelpers";
3
import { CRng, mixSeeds } from "@/src/services/rngService";
3
import { mixSeeds, SRng } from "@/src/services/rngService";
4
4
import { IMongoDate } from "@/src/types/commonTypes";
5
5
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
6
6
import { ExportVendors, IRange } from "warframe-public-export-plus";
@@ -204,7 +204,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
204
204
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
205
205
const cycleDuration = vendorInfo.cycleDuration;
206
206
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
207
const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
207
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
208
208
const manifest = ExportVendors[vendorInfo.TypeName];
209
209
const offersToAdd = [];
210
210
if (manifest.numItems && !manifest.isOneBinPerCycle) {
@@ -247,8 +247,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
247
247
$oid:
248
248
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
249
249
vendorInfo._id.$oid.substring(8, 16) +
250
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
251
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
250
rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0")
252
251
}
253
252
};
254
253
if (rawItem.numRandomItemPrices) {
@@ -283,9 +282,9 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
283
282
item.PremiumPrice = [value, value];
284
283
}
285
284
if (vendorInfo.RandomSeedType) {
286
item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
285
item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff);
287
286
if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
288
const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
287
const highDword = rng.randomInt(0, 0xffff_ffff);
289
288
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
290
289
}
291
290
}
@@ -5,7 +5,7 @@ import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMiss
5
5
import { buildConfig } from "@/src/services/buildConfigService";
6
6
import { unixTimesInMs } from "@/src/constants/timeConstants";
7
7
import { config } from "@/src/services/configService";
8
import { CRng, SRng } from "@/src/services/rngService";
8
import { SRng } from "@/src/services/rngService";
9
9
import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
10
10
import {
11
11
ICalendarDay,
@@ -193,7 +193,7 @@ const pushSyndicateMissions = (
193
193
): void => {
194
194
const nodeOptions: string[] = [...syndicateMissions];
195
195
196
const rng = new CRng(seed);
196
const rng = new SRng(seed);
197
197
const nodes: string[] = [];
198
198
for (let i = 0; i != 6; ++i) {
199
199
const index = rng.randomInt(0, nodeOptions.length - 1);
@@ -235,8 +235,8 @@ const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): voi
235
235
};
236
236
237
237
export const getSortie = (day: number): ISortie => {
238
const seed = new CRng(day).randomInt(0, 100_000);
239
const rng = new CRng(seed);
238
const seed = new SRng(day).randomInt(0, 100_000);
239
const rng = new SRng(seed);
240
240
241
241
const boss = rng.randomElement(sortieBosses)!;
242
242
@@ -351,7 +351,7 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
351
351
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
352
352
const dayStart = EPOCH + day * 86400000;
353
353
const dayEnd = EPOCH + (day + 3) * 86400000;
354
const rng = new CRng(new CRng(day).randomInt(0, 100_000));
354
const rng = new SRng(new SRng(day).randomInt(0, 100_000));
355
355
return {
356
356
_id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
357
357
Daily: true,
@@ -371,7 +371,7 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge =>
371
371
const weekStart = EPOCH + week * 604800000;
372
372
const weekEnd = weekStart + 604800000;
373
373
const challengeId = week * 7 + id;
374
const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000));
374
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
375
375
return {
376
376
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
377
377
Activation: { $date: { $numberLong: weekStart.toString() } },
@@ -388,7 +388,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
388
388
const weekStart = EPOCH + week * 604800000;
389
389
const weekEnd = weekStart + 604800000;
390
390
const challengeId = week * 7 + id;
391
const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000));
391
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
392
392
return {
393
393
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
394
394
Activation: { $date: { $numberLong: weekStart.toString() } },
@@ -432,12 +432,12 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
432
432
433
433
// TODO: xpAmounts need to be calculated based on the jobType somehow?
434
434
435
const seed = new CRng(bountyCycle).randomInt(0, 100_000);
435
const seed = new SRng(bountyCycle).randomInt(0, 100_000);
436
436
const bountyCycleStart = bountyCycle * 9000000;
437
437
const bountyCycleEnd = bountyCycleStart + 9000000;
438
438
439
439
{
440
const rng = new CRng(seed);
440
const rng = new SRng(seed);
441
441
syndicateMissions.push({
442
442
_id: {
443
443
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
@@ -509,7 +509,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
509
509
}
510
510
511
511
{
512
const rng = new CRng(seed);
512
const rng = new SRng(seed);
513
513
syndicateMissions.push({
514
514
_id: {
515
515
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
@@ -581,7 +581,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
581
581
}
582
582
583
583
{
584
const rng = new CRng(seed);
584
const rng = new SRng(seed);
585
585
syndicateMissions.push({
586
586
_id: {
587
587
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
@@ -701,7 +701,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
701
701
//logger.debug(`birthday on day ${day}`);
702
702
eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0
703
703
}
704
const rng = new CRng(new CRng(week).randomInt(0, 100_000));
704
const rng = new SRng(new SRng(week).randomInt(0, 100_000));
705
705
const challenges = [
706
706
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy",
707
707
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium",
@@ -982,7 +982,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
982
982
}
983
983
984
984
// Elite Sanctuary Onslaught cycling every week
985
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(BigInt(week)).randomInt(0, 0xff_ffff);
985
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff);
986
986
987
987
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
988
988
let bountyCycle = Math.trunc(Date.now() / 9000000);
@@ -1068,7 +1068,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
1068
1068
1069
1069
// The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
1070
1070
const sdy = Date.now() >= rollover ? day : day - 1;
1071
const rng = new CRng(sdy);
1071
const rng = new SRng(sdy);
1072
1072
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
1073
1073
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
1074
1074
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate");
@@ -1184,8 +1184,8 @@ export const getLiteSortie = (week: number): ILiteSortie => {
1184
1184
}
1185
1185
}
1186
1186
1187
const seed = new CRng(week).randomInt(0, 100_000);
1188
const rng = new CRng(seed);
1187
const seed = new SRng(week).randomInt(0, 100_000);
1188
const rng = new SRng(seed);
1189
1189
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
1190
1190
const firstNode = nodes[firstNodeIndex];
1191
1191
nodes.splice(firstNodeIndex, 1);