chore: use 64-bit RNG everywhere (#2030)
Closes #2026 Reviewed-on: OpenWF/SpaceNinjaServer#2030 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									1084932afb
								
							
						
					
					
						commit
						3fc2dccf81
					
				@ -2,7 +2,7 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { ExportWeapons } from "warframe-public-export-plus";
 | 
			
		||||
import { IMongoDate } from "@/src/types/commonTypes";
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { CRng } from "@/src/services/rngService";
 | 
			
		||||
import { SRng } from "@/src/services/rngService";
 | 
			
		||||
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import {
 | 
			
		||||
@ -140,7 +140,7 @@ const getModularWeaponSale = (
 | 
			
		||||
    partTypes: string[],
 | 
			
		||||
    getItemType: (parts: string[]) => string
 | 
			
		||||
): IModularWeaponSaleInfo => {
 | 
			
		||||
    const rng = new CRng(day);
 | 
			
		||||
    const rng = new SRng(day);
 | 
			
		||||
    const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
 | 
			
		||||
    let partsCost = 0;
 | 
			
		||||
    for (const part of parts) {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json";
 | 
			
		||||
import { IInventoryChanges } from "../types/purchaseTypes";
 | 
			
		||||
import { TAccountDocument } from "./loginService";
 | 
			
		||||
import { CRng, mixSeeds } from "./rngService";
 | 
			
		||||
import { mixSeeds, SRng } from "./rngService";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
 | 
			
		||||
import { addBooster, updateCurrency } from "./inventoryService";
 | 
			
		||||
import { handleStoreItemAcquisition } from "./purchaseService";
 | 
			
		||||
@ -49,8 +49,8 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu
 | 
			
		||||
// Always produces the same result for the same account _id & LoginDays pair.
 | 
			
		||||
export const isLoginRewardAChoice = (account: TAccountDocument): boolean => {
 | 
			
		||||
    const accountSeed = parseInt(account._id.toString().substring(16), 16);
 | 
			
		||||
    const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
 | 
			
		||||
    return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
 | 
			
		||||
    const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
 | 
			
		||||
    return rng.randomFloat() < 0.25;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Always produces the same result for the same account _id & LoginDays pair.
 | 
			
		||||
@ -59,8 +59,8 @@ export const getRandomLoginRewards = (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument
 | 
			
		||||
): ILoginReward[] => {
 | 
			
		||||
    const accountSeed = parseInt(account._id.toString().substring(16), 16);
 | 
			
		||||
    const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
 | 
			
		||||
    const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
 | 
			
		||||
    const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
 | 
			
		||||
    const pick_a_door = rng.randomFloat() < 0.25;
 | 
			
		||||
    const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)];
 | 
			
		||||
    if (pick_a_door) {
 | 
			
		||||
        do {
 | 
			
		||||
@ -73,7 +73,7 @@ export const getRandomLoginRewards = (
 | 
			
		||||
    return rewards;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
 | 
			
		||||
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
 | 
			
		||||
    const reward = rng.randomReward(randomRewards)!;
 | 
			
		||||
    //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
 | 
			
		||||
    if (reward.RewardType == "RT_RANDOM_RECIPE") {
 | 
			
		||||
 | 
			
		||||
@ -86,54 +86,12 @@ export const mixSeeds = (seed1: number, seed2: number): number => {
 | 
			
		||||
    return seed >>> 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
 | 
			
		||||
export class CRng {
 | 
			
		||||
    state: number;
 | 
			
		||||
 | 
			
		||||
    constructor(seed: number = 1) {
 | 
			
		||||
        this.state = seed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    random(): number {
 | 
			
		||||
        this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
 | 
			
		||||
        return (this.state & 0x3fffffff) / 0x3fffffff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    randomInt(min: number, max: number): number {
 | 
			
		||||
        const diff = max - min;
 | 
			
		||||
        if (diff != 0) {
 | 
			
		||||
            if (diff < 0) {
 | 
			
		||||
                throw new Error(`max must be greater than min`);
 | 
			
		||||
            }
 | 
			
		||||
            if (diff > 0x3fffffff) {
 | 
			
		||||
                throw new Error(`insufficient entropy`);
 | 
			
		||||
            }
 | 
			
		||||
            min += Math.floor(this.random() * (diff + 1));
 | 
			
		||||
        }
 | 
			
		||||
        return min;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    randomElement<T>(arr: readonly T[]): T | undefined {
 | 
			
		||||
        return arr[Math.floor(this.random() * arr.length)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
 | 
			
		||||
        return getRewardAtPercentage(pool, this.random());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    churnSeed(its: number): void {
 | 
			
		||||
        while (its--) {
 | 
			
		||||
            this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
 | 
			
		||||
// Seeded RNG with identical results to the game client. Based on work by Donald Knuth.
 | 
			
		||||
export class SRng {
 | 
			
		||||
    state: bigint;
 | 
			
		||||
 | 
			
		||||
    constructor(seed: bigint) {
 | 
			
		||||
        this.state = seed;
 | 
			
		||||
    constructor(seed: bigint | number) {
 | 
			
		||||
        this.state = BigInt(seed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    randomInt(min: number, max: number): number {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
			
		||||
import { catBreadHash } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { CRng, mixSeeds } from "@/src/services/rngService";
 | 
			
		||||
import { mixSeeds, SRng } from "@/src/services/rngService";
 | 
			
		||||
import { IMongoDate } from "@/src/types/commonTypes";
 | 
			
		||||
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
 | 
			
		||||
import { ExportVendors, IRange } from "warframe-public-export-plus";
 | 
			
		||||
@ -204,7 +204,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
        const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
 | 
			
		||||
        const cycleDuration = vendorInfo.cycleDuration;
 | 
			
		||||
        const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
 | 
			
		||||
        const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
 | 
			
		||||
        const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
 | 
			
		||||
        const manifest = ExportVendors[vendorInfo.TypeName];
 | 
			
		||||
        const offersToAdd = [];
 | 
			
		||||
        if (manifest.numItems && !manifest.isOneBinPerCycle) {
 | 
			
		||||
@ -247,8 +247,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
                    $oid:
 | 
			
		||||
                        ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
 | 
			
		||||
                        vendorInfo._id.$oid.substring(8, 16) +
 | 
			
		||||
                        rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
 | 
			
		||||
                        rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
 | 
			
		||||
                        rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0")
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            if (rawItem.numRandomItemPrices) {
 | 
			
		||||
@ -283,9 +282,9 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
                item.PremiumPrice = [value, value];
 | 
			
		||||
            }
 | 
			
		||||
            if (vendorInfo.RandomSeedType) {
 | 
			
		||||
                item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
 | 
			
		||||
                item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff);
 | 
			
		||||
                if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
 | 
			
		||||
                    const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
 | 
			
		||||
                    const highDword = rng.randomInt(0, 0xffff_ffff);
 | 
			
		||||
                    item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMiss
 | 
			
		||||
import { buildConfig } from "@/src/services/buildConfigService";
 | 
			
		||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { CRng, SRng } from "@/src/services/rngService";
 | 
			
		||||
import { SRng } from "@/src/services/rngService";
 | 
			
		||||
import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
 | 
			
		||||
import {
 | 
			
		||||
    ICalendarDay,
 | 
			
		||||
@ -193,7 +193,7 @@ const pushSyndicateMissions = (
 | 
			
		||||
): void => {
 | 
			
		||||
    const nodeOptions: string[] = [...syndicateMissions];
 | 
			
		||||
 | 
			
		||||
    const rng = new CRng(seed);
 | 
			
		||||
    const rng = new SRng(seed);
 | 
			
		||||
    const nodes: string[] = [];
 | 
			
		||||
    for (let i = 0; i != 6; ++i) {
 | 
			
		||||
        const index = rng.randomInt(0, nodeOptions.length - 1);
 | 
			
		||||
@ -235,8 +235,8 @@ const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): voi
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getSortie = (day: number): ISortie => {
 | 
			
		||||
    const seed = new CRng(day).randomInt(0, 100_000);
 | 
			
		||||
    const rng = new CRng(seed);
 | 
			
		||||
    const seed = new SRng(day).randomInt(0, 100_000);
 | 
			
		||||
    const rng = new SRng(seed);
 | 
			
		||||
 | 
			
		||||
    const boss = rng.randomElement(sortieBosses)!;
 | 
			
		||||
 | 
			
		||||
@ -351,7 +351,7 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
 | 
			
		||||
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
 | 
			
		||||
    const dayStart = EPOCH + day * 86400000;
 | 
			
		||||
    const dayEnd = EPOCH + (day + 3) * 86400000;
 | 
			
		||||
    const rng = new CRng(new CRng(day).randomInt(0, 100_000));
 | 
			
		||||
    const rng = new SRng(new SRng(day).randomInt(0, 100_000));
 | 
			
		||||
    return {
 | 
			
		||||
        _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
 | 
			
		||||
        Daily: true,
 | 
			
		||||
@ -371,7 +371,7 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge =>
 | 
			
		||||
    const weekStart = EPOCH + week * 604800000;
 | 
			
		||||
    const weekEnd = weekStart + 604800000;
 | 
			
		||||
    const challengeId = week * 7 + id;
 | 
			
		||||
    const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000));
 | 
			
		||||
    const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
 | 
			
		||||
    return {
 | 
			
		||||
        _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
 | 
			
		||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
			
		||||
@ -388,7 +388,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
 | 
			
		||||
    const weekStart = EPOCH + week * 604800000;
 | 
			
		||||
    const weekEnd = weekStart + 604800000;
 | 
			
		||||
    const challengeId = week * 7 + id;
 | 
			
		||||
    const rng = new CRng(new CRng(challengeId).randomInt(0, 100_000));
 | 
			
		||||
    const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
 | 
			
		||||
    return {
 | 
			
		||||
        _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
 | 
			
		||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
			
		||||
@ -432,12 +432,12 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
 | 
			
		||||
 | 
			
		||||
    // TODO: xpAmounts need to be calculated based on the jobType somehow?
 | 
			
		||||
 | 
			
		||||
    const seed = new CRng(bountyCycle).randomInt(0, 100_000);
 | 
			
		||||
    const seed = new SRng(bountyCycle).randomInt(0, 100_000);
 | 
			
		||||
    const bountyCycleStart = bountyCycle * 9000000;
 | 
			
		||||
    const bountyCycleEnd = bountyCycleStart + 9000000;
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        const rng = new CRng(seed);
 | 
			
		||||
        const rng = new SRng(seed);
 | 
			
		||||
        syndicateMissions.push({
 | 
			
		||||
            _id: {
 | 
			
		||||
                $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
 | 
			
		||||
@ -509,7 +509,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        const rng = new CRng(seed);
 | 
			
		||||
        const rng = new SRng(seed);
 | 
			
		||||
        syndicateMissions.push({
 | 
			
		||||
            _id: {
 | 
			
		||||
                $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
 | 
			
		||||
@ -581,7 +581,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        const rng = new CRng(seed);
 | 
			
		||||
        const rng = new SRng(seed);
 | 
			
		||||
        syndicateMissions.push({
 | 
			
		||||
            _id: {
 | 
			
		||||
                $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
 | 
			
		||||
@ -701,7 +701,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
 | 
			
		||||
        //logger.debug(`birthday on day ${day}`);
 | 
			
		||||
        eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0
 | 
			
		||||
    }
 | 
			
		||||
    const rng = new CRng(new CRng(week).randomInt(0, 100_000));
 | 
			
		||||
    const rng = new SRng(new SRng(week).randomInt(0, 100_000));
 | 
			
		||||
    const challenges = [
 | 
			
		||||
        "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy",
 | 
			
		||||
        "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium",
 | 
			
		||||
@ -982,7 +982,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Elite Sanctuary Onslaught cycling every week
 | 
			
		||||
    worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(BigInt(week)).randomInt(0, 0xff_ffff);
 | 
			
		||||
    worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff);
 | 
			
		||||
 | 
			
		||||
    // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
 | 
			
		||||
    let bountyCycle = Math.trunc(Date.now() / 9000000);
 | 
			
		||||
@ -1068,7 +1068,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
 | 
			
		||||
        // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
 | 
			
		||||
        const sdy = Date.now() >= rollover ? day : day - 1;
 | 
			
		||||
        const rng = new CRng(sdy);
 | 
			
		||||
        const rng = new SRng(sdy);
 | 
			
		||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
 | 
			
		||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
 | 
			
		||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate");
 | 
			
		||||
@ -1184,8 +1184,8 @@ export const getLiteSortie = (week: number): ILiteSortie => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const seed = new CRng(week).randomInt(0, 100_000);
 | 
			
		||||
    const rng = new CRng(seed);
 | 
			
		||||
    const seed = new SRng(week).randomInt(0, 100_000);
 | 
			
		||||
    const rng = new SRng(seed);
 | 
			
		||||
    const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
 | 
			
		||||
    const firstNode = nodes[firstNodeIndex];
 | 
			
		||||
    nodes.splice(firstNodeIndex, 1);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user