Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce xoshiro RNG to generate dungeon seeds #7030

Merged
merged 5 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1821,7 +1821,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();
StephenCWills marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -2134,8 +2134,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
Loading