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

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

代码差异

5 个文件 +32 -75
Modified src/controllers/api/modularWeaponSaleController.ts +2 -2
@@ -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) {
Modified src/services/loginRewardService.ts +6 -6
@@ -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") {
Modified src/services/rngService.ts +3 -45
@@ -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 {
Modified src/services/serversideVendorsService.ts +5 -6
@@ -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 }
Modified src/services/worldStateService.ts +16 -16
@@ -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);