Browse Source

Introduce xoshiro RNG to generate dungeon seeds (#7030)

pull/6507/head
Stephen C. Wills 2 years ago committed by GitHub
parent
commit
cfe9a8ccdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 54
      Source/engine/random.cpp
  2. 158
      Source/engine/random.hpp
  3. 2
      Source/loadsave.cpp
  4. 2
      Source/msg.cpp
  5. 9
      Source/multi.cpp
  6. 5
      Source/multi.h
  7. 2
      Source/stores.cpp
  8. 8
      test/random_test.cpp

54
Source/engine/random.cpp

@ -1,7 +1,10 @@
#include "engine/random.hpp" #include "engine/random.hpp"
#include <bit>
#include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <limits> #include <limits>
#include <random> #include <random>
@ -13,6 +16,51 @@ uint32_t sglGameSeed;
/** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */ /** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */
std::linear_congruential_engine<uint32_t, 0x015A4E35, 1, 0> diabloGenerator; 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) void SetRndSeed(uint32_t seed)
{ {
diabloGenerator.seed(seed); diabloGenerator.seed(seed);
@ -27,12 +75,12 @@ uint32_t GetLCGEngineState()
void DiscardRandomValues(unsigned count) void DiscardRandomValues(unsigned count)
{ {
while (count != 0) { while (count != 0) {
GenerateSeed(); GenerateRandomNumber();
count--; count--;
} }
} }
uint32_t GenerateSeed() uint32_t GenerateRandomNumber()
{ {
sglGameSeed = diabloGenerator(); sglGameSeed = diabloGenerator();
return sglGameSeed; return sglGameSeed;
@ -40,7 +88,7 @@ uint32_t GenerateSeed()
int32_t AdvanceRndSeed() 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 // 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); return seed == std::numeric_limits<int32_t>::min() ? std::numeric_limits<int32_t>::min() : std::abs(seed);
} }

158
Source/engine/random.hpp

@ -136,6 +136,162 @@ public:
} }
}; };
// 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 * @brief Set the state of the RandomNumberEngine used by the base game to the specific seed
* @param seed New engine state * @param seed New engine state
@ -163,7 +319,7 @@ void DiscardRandomValues(unsigned count);
/** /**
* @brief Advances the global RandomNumberEngine state and returns the new value * @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 * @brief Generates a random non-negative integer (most of the time) using the vanilla RNG

2
Source/loadsave.cpp

@ -1839,7 +1839,7 @@ void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData)
DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level
if (leveltype == DTYPE_TOWN) if (leveltype == DTYPE_TOWN)
DungeonSeeds[0] = AdvanceRndSeed(); DungeonSeeds[0] = GenerateSeed();
char szName[MaxMpqPathSize]; char szName[MaxMpqPathSize];
GetTempLevelNames(szName); GetTempLevelNames(szName);

2
Source/msg.cpp

@ -750,7 +750,7 @@ void DeltaLeaveSync(uint8_t bLevel)
if (!gbIsMultiplayer) if (!gbIsMultiplayer)
return; return;
if (leveltype == DTYPE_TOWN) { if (leveltype == DTYPE_TOWN) {
DungeonSeeds[0] = AdvanceRndSeed(); DungeonSeeds[0] = GenerateSeed();
return; return;
} }

9
Source/multi.cpp

@ -6,7 +6,6 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <ctime>
#include <string_view> #include <string_view>
#include <SDL.h> #include <SDL.h>
@ -485,8 +484,10 @@ bool InitMulti(GameData *gameData)
void InitGameInfo() void InitGameInfo()
{ {
xoshiro128plusplus gameGenerator = ReserveSeedSequence();
gameGenerator.save(sgGameInitInfo.gameSeed);
sgGameInitInfo.size = sizeof(sgGameInitInfo); sgGameInitInfo.size = sizeof(sgGameInitInfo);
sgGameInitInfo.dwSeed = static_cast<uint32_t>(time(nullptr));
sgGameInitInfo.programid = GAME_ID; sgGameInitInfo.programid = GAME_ID;
sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR; sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR;
sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR; sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR;
@ -787,11 +788,11 @@ bool NetInit(bool bSinglePlayer)
NetClose(); NetClose();
gbSelectProvider = false; gbSelectProvider = false;
} }
SetRndSeed(sgGameInitInfo.dwSeed); xoshiro128plusplus gameGenerator(sgGameInitInfo.gameSeed);
gnTickDelay = 1000 / sgGameInitInfo.nTickRate; gnTickDelay = 1000 / sgGameInitInfo.nTickRate;
for (int i = 0; i < NUMLEVELS; i++) { for (int i = 0; i < NUMLEVELS; i++) {
DungeonSeeds[i] = AdvanceRndSeed(); DungeonSeeds[i] = gameGenerator.next();
LevelSeeds[i] = std::nullopt; LevelSeeds[i] = std::nullopt;
} }
PublicGame = DvlNet_IsPublicGame(); PublicGame = DvlNet_IsPublicGame();

5
Source/multi.h

@ -22,8 +22,7 @@ struct Player;
struct GameData { struct GameData {
int32_t size; int32_t size;
/** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */ uint8_t reserved[4];
uint32_t dwSeed;
uint32_t programid; uint32_t programid;
uint8_t versionMajor; uint8_t versionMajor;
uint8_t versionMinor; uint8_t versionMinor;
@ -35,6 +34,8 @@ struct GameData {
uint8_t bCowQuest; uint8_t bCowQuest;
uint8_t bFriendlyFire; uint8_t bFriendlyFire;
uint8_t fullQuests; 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) */ /* @brief Contains info of running public game (for game list browsing) */

2
Source/stores.cpp

@ -2132,8 +2132,6 @@ void SetupTownStores()
if (myPlayer._pLvlVisited[i]) if (myPlayer._pLvlVisited[i])
l = i; l = i;
} }
} else {
SetRndSeed(DungeonSeeds[currlevel] * SDL_GetTicks());
} }
l = std::clamp(l + 2, 6, 16); l = std::clamp(l + 2, 6, 16);

8
test/random_test.cpp

@ -17,10 +17,10 @@ TEST(RandomTest, RandomEngineParams)
SetRndSeed(0); SetRndSeed(0);
// Starting from a seed of 0 means the multiplicand is dropped and the state advances by increment only // 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. // 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 // 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. // 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.
@ -28,9 +28,9 @@ TEST(RandomTest, RandomEngineParams)
DiscardRandomValues(9997); DiscardRandomValues(9997);
uint32_t expectedState = 3495122800U; 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; 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) TEST(RandomTest, AbsDistribution)

Loading…
Cancel
Save