Skip to content

Commit

Permalink
Introduce xoshiro RNG to generate dungeon seeds (#7030)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenCWills authored Sep 17, 2024
1 parent 8970eaa commit cfe9a8c
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 18 deletions.
54 changes: 51 additions & 3 deletions Source/engine/random.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include "engine/random.hpp"

#include <bit>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <random>

Expand All @@ -13,6 +16,51 @@ uint32_t sglGameSeed;
/** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */
std::linear_congruential_engine<uint32_t, 0x015A4E35, 1, 0> diabloGenerator;

/** Xoshiro pseudo-random number generator to provide less predictable seeds */
xoshiro128plusplus seedGenerator;

uint32_t xoshiro128plusplus::next()
{
const uint32_t result = std::rotl(s[0] + s[3], 7) + s[0];

const uint32_t t = s[1] << 9;

s[2] ^= s[0];
s[3] ^= s[1];
s[1] ^= s[2];
s[0] ^= s[3];

s[2] ^= t;

s[3] = std::rotl(s[3], 11);

return result;
}

uint64_t xoshiro128plusplus::timeSeed()
{
auto now = std::chrono::system_clock::now();
auto nano = std::chrono::nanoseconds(now.time_since_epoch());
return static_cast<uint64_t>(nano.count());
}

void xoshiro128plusplus::copy(state &dst, const state &src)
{
memcpy(dst, src, sizeof(dst));
}

xoshiro128plusplus ReserveSeedSequence()
{
xoshiro128plusplus reserved = seedGenerator;
seedGenerator.jump();
return reserved;
}

uint32_t GenerateSeed()
{
return seedGenerator.next();
}

void SetRndSeed(uint32_t seed)
{
diabloGenerator.seed(seed);
Expand All @@ -27,20 +75,20 @@ uint32_t GetLCGEngineState()
void DiscardRandomValues(unsigned count)
{
while (count != 0) {
GenerateSeed();
GenerateRandomNumber();
count--;
}
}

uint32_t GenerateSeed()
uint32_t GenerateRandomNumber()
{
sglGameSeed = diabloGenerator();
return sglGameSeed;
}

int32_t AdvanceRndSeed()
{
const int32_t seed = static_cast<int32_t>(GenerateSeed());
const int32_t seed = static_cast<int32_t>(GenerateRandomNumber());
// since abs(INT_MIN) is undefined behavior, handle this value specially
return seed == std::numeric_limits<int32_t>::min() ? std::numeric_limits<int32_t>::min() : std::abs(seed);
}
Expand Down
158 changes: 157 additions & 1 deletion Source/engine/random.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,162 @@ class DiabloGenerator {
}
};

// Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008
// https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68
// and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
//
// See also:
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
// https://doi.org/10.1145/2714064.2660195
class SplitMix32 {
uint32_t state;

public:
SplitMix32(uint32_t state)
: state(state)
{
}

uint32_t next()
{
uint32_t z = (state += 0x9e3779b9);
z = (z ^ (z >> 16)) * 0x85ebca6b;
z = (z ^ (z >> 13)) * 0xc2b2ae35;
return z ^ (z >> 16);
}

void generate(uint32_t *begin, const uint32_t *end)
{
while (begin != end) {
*begin = next();
++begin;
}
}
};

// Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
//
// See also:
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
// https://doi.org/10.1145/2714064.2660195
class SplitMix64 {
uint64_t state;

public:
SplitMix64(uint64_t state)
: state(state)
{
}

uint64_t next()
{
uint64_t z = (state += 0x9e3779b97f4a7c15);
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
return z ^ (z >> 31);
}

void generate(uint64_t *begin, const uint64_t *end)
{
while (begin != end) {
*begin = next();
++begin;
}
}
};

/** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */
class xoshiro128plusplus {
public:
typedef uint32_t state[4];

xoshiro128plusplus() { seed(); }
xoshiro128plusplus(const state &s) { copy(this->s, s); }
xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); }
xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); }

uint32_t next();

/* This is the jump function for the generator. It is equivalent
to 2^64 calls to next(); it can be used to generate 2^64
non-overlapping subsequences for parallel computations. */
void jump()
{
static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b };

uint32_t s0 = 0;
uint32_t s1 = 0;
uint32_t s2 = 0;
uint32_t s3 = 0;
for (const uint32_t entry : JUMP)
for (int b = 0; b < 32; b++) {
if (entry & UINT32_C(1) << b) {
s0 ^= s[0];
s1 ^= s[1];
s2 ^= s[2];
s3 ^= s[3];
}
next();
}

s[0] = s0;
s[1] = s1;
s[2] = s2;
s[3] = s3;
}

void save(state &s) const
{
copy(s, this->s);
}

private:
state s;

void seed(uint64_t value)
{
uint64_t seeds[2];
SplitMix64 seedSequence { value };
seedSequence.generate(seeds, seeds + 2);

s[0] = static_cast<uint32_t>(seeds[0] >> 32);
s[1] = static_cast<uint32_t>(seeds[0]);
s[2] = static_cast<uint32_t>(seeds[1] >> 32);
s[3] = static_cast<uint32_t>(seeds[1]);
}

void seed(uint32_t value)
{
SplitMix32 seedSequence { value };
seedSequence.generate(s, s + 4);
}

void seed()
{
seed(timeSeed());

static std::random_device rd;
std::uniform_int_distribution<uint32_t> dist;
for (uint32_t &cell : s)
cell ^= dist(rd);
}

static uint64_t timeSeed();
static void copy(state &dst, const state &src);
};

/**
* @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions
*/
xoshiro128plusplus ReserveSeedSequence();

/**
* @brief Advances the global seed generator state and returns the new value
*/
uint32_t GenerateSeed();

/**
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed
* @param seed New engine state
Expand Down Expand Up @@ -163,7 +319,7 @@ void DiscardRandomValues(unsigned count);
/**
* @brief Advances the global RandomNumberEngine state and returns the new value
*/
uint32_t GenerateSeed();
uint32_t GenerateRandomNumber();

/**
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG
Expand Down
2 changes: 1 addition & 1 deletion Source/loadsave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1839,7 +1839,7 @@ void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData)
DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level

if (leveltype == DTYPE_TOWN)
DungeonSeeds[0] = AdvanceRndSeed();
DungeonSeeds[0] = GenerateSeed();

char szName[MaxMpqPathSize];
GetTempLevelNames(szName);
Expand Down
2 changes: 1 addition & 1 deletion Source/msg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ void DeltaLeaveSync(uint8_t bLevel)
if (!gbIsMultiplayer)
return;
if (leveltype == DTYPE_TOWN) {
DungeonSeeds[0] = AdvanceRndSeed();
DungeonSeeds[0] = GenerateSeed();
return;
}

Expand Down
9 changes: 5 additions & 4 deletions Source/multi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

#include <cstddef>
#include <cstdint>
#include <ctime>
#include <string_view>

#include <SDL.h>
Expand Down Expand Up @@ -485,8 +484,10 @@ bool InitMulti(GameData *gameData)

void InitGameInfo()
{
xoshiro128plusplus gameGenerator = ReserveSeedSequence();
gameGenerator.save(sgGameInitInfo.gameSeed);

sgGameInitInfo.size = sizeof(sgGameInitInfo);
sgGameInitInfo.dwSeed = static_cast<uint32_t>(time(nullptr));
sgGameInitInfo.programid = GAME_ID;
sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR;
sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR;
Expand Down Expand Up @@ -787,11 +788,11 @@ bool NetInit(bool bSinglePlayer)
NetClose();
gbSelectProvider = false;
}
SetRndSeed(sgGameInitInfo.dwSeed);
xoshiro128plusplus gameGenerator(sgGameInitInfo.gameSeed);
gnTickDelay = 1000 / sgGameInitInfo.nTickRate;

for (int i = 0; i < NUMLEVELS; i++) {
DungeonSeeds[i] = AdvanceRndSeed();
DungeonSeeds[i] = gameGenerator.next();
LevelSeeds[i] = std::nullopt;
}
PublicGame = DvlNet_IsPublicGame();
Expand Down
5 changes: 3 additions & 2 deletions Source/multi.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ struct Player;

struct GameData {
int32_t size;
/** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */
uint32_t dwSeed;
uint8_t reserved[4];
uint32_t programid;
uint8_t versionMajor;
uint8_t versionMinor;
Expand All @@ -35,6 +34,8 @@ struct GameData {
uint8_t bCowQuest;
uint8_t bFriendlyFire;
uint8_t fullQuests;
/** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */
uint32_t gameSeed[4];
};

/* @brief Contains info of running public game (for game list browsing) */
Expand Down
2 changes: 0 additions & 2 deletions Source/stores.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2132,8 +2132,6 @@ void SetupTownStores()
if (myPlayer._pLvlVisited[i])
l = i;
}
} else {
SetRndSeed(DungeonSeeds[currlevel] * SDL_GetTicks());
}

l = std::clamp(l + 2, 6, 16);
Expand Down
8 changes: 4 additions & 4 deletions test/random_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ TEST(RandomTest, RandomEngineParams)
SetRndSeed(0);

// Starting from a seed of 0 means the multiplicand is dropped and the state advances by increment only
ASSERT_EQ(GenerateSeed(), increment) << "Increment factor is incorrect";
ASSERT_EQ(GenerateRandomNumber(), increment) << "Increment factor is incorrect";

// LCGs use a formula of mult * seed + inc. Using a long form in the code to document the expected factors.
ASSERT_EQ(GenerateSeed(), (multiplicand * 1) + increment) << "Multiplicand factor is incorrect";
ASSERT_EQ(GenerateRandomNumber(), (multiplicand * 1) + increment) << "Multiplicand factor is incorrect";

// C++11 defines the default seed for a LCG engine as 1. The ten thousandth value is commonly used for sanity checking
// a sequence, so as we've had one round since state 1 we need to discard another 9998 values to get to the 10000th state.
// To make off by one errors more visible test the 9999th value as well as 10000th
DiscardRandomValues(9997);

uint32_t expectedState = 3495122800U;
EXPECT_EQ(GenerateSeed(), expectedState) << "Wrong engine state after 9999 invocations";
EXPECT_EQ(GenerateRandomNumber(), expectedState) << "Wrong engine state after 9999 invocations";
expectedState = 3007658545U;
ASSERT_EQ(GenerateSeed(), expectedState) << "Wrong engine state after 10000 invocations";
ASSERT_EQ(GenerateRandomNumber(), expectedState) << "Wrong engine state after 10000 invocations";
}

TEST(RandomTest, AbsDistribution)
Expand Down

0 comments on commit cfe9a8c

Please sign in to comment.