diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 9c59ee6940c..65977ef3341 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -40,6 +40,7 @@ #include "ship/shipfx.h" #include "ship/shiphit.h" #include "stats/scoring.h" +#include "utils/Random.h" #include "weapon/weapon.h" #include @@ -1900,8 +1901,8 @@ static void asteroid_maybe_break_up(object *pasteroid_obj) roids_to_create.push_back(split->asteroid_type); } - std::random_device rd; - std::shuffle(roids_to_create.begin(), roids_to_create.end(), std::mt19937(rd())); + std::mt19937 shuffle_rng(util::Random::shuffle_seed()); + std::shuffle(roids_to_create.begin(), roids_to_create.end(), shuffle_rng); size_t total_roids = roids_to_create.size(); for (size_t i = 0; i < total_roids; i++) { diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 2ac42ba0db2..b047c495565 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -15506,8 +15506,8 @@ void sexp_send_builtin_message(int n) { } if (shuffle) { - std::random_device rd; - std::shuffle(sender_names.begin(), sender_names.end(), std::mt19937(rd())); + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(sender_names.begin(), sender_names.end(), rng); } auto subject = have_subject ? subject_entry->shipp() : nullptr; diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 21f148802b5..61efc191c57 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -11158,8 +11158,8 @@ static void ship_set_default_weapons(ship *shipp, ship_info *sip) for (int fp = 0; fp < pm->gun_banks[i].num_slots; fp++) { fpi.push_back(fp); } - std::random_device rd; - std::shuffle(fpi.begin(), fpi.end(), std::mt19937(rd())); + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(fpi.begin(), fpi.end(), rng); } swp->num_secondary_banks = sip->num_secondary_banks; @@ -11745,8 +11745,8 @@ static void ship_model_change(int n, int ship_type) for (int fp = 0; fp < pm->gun_banks[bank_i].num_slots; fp++) { fpi.push_back(fp); } - std::random_device rd; - std::shuffle(fpi.begin(), fpi.end(), std::mt19937(rd())); + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(fpi.begin(), fpi.end(), rng); } model_delete_instance(sp->model_instance_num); @@ -13496,8 +13496,8 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) for (int fp = 0; fp < num_slots; fp++) { fpi.push_back(fp); } - std::random_device rd; - std::shuffle(fpi.begin(), fpi.end(), std::mt19937(rd())); + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(fpi.begin(), fpi.end(), rng); } // The energy-consumption code executes even for ballistic primaries, because @@ -13783,26 +13783,26 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) switch (firing_pattern) { case FiringPattern::RANDOM_EXHAUSTIVE: { if (num_slots < (swp->primary_firepoint_next_to_fire_index[bank_to_fire] + point_count)) { - std::random_device rd; - std::shuffle(&swp->primary_firepoint_indices[bank_to_fire][0], &swp->primary_firepoint_indices[bank_to_fire][swp->primary_firepoint_next_to_fire_index[bank_to_fire]-1], std::mt19937(rd())); //NOLINT + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(&swp->primary_firepoint_indices[bank_to_fire][0], &swp->primary_firepoint_indices[bank_to_fire][swp->primary_firepoint_next_to_fire_index[bank_to_fire]-1], rng); //NOLINT } else if (swp->primary_firepoint_next_to_fire_index[bank_to_fire] < point_count) { - std::random_device rd; - std::shuffle(swp->primary_firepoint_indices[bank_to_fire].begin(), swp->primary_firepoint_indices[bank_to_fire].end(), std::mt19937(rd())); + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(swp->primary_firepoint_indices[bank_to_fire].begin(), swp->primary_firepoint_indices[bank_to_fire].end(), rng); } break; } case FiringPattern::RANDOM_NONREPEATING: { int shuffle_start = MIN(point_count, num_slots - point_count); - std::random_device rd; + std::mt19937 rng(util::Random::shuffle_seed()); auto middle_iterator = swp->primary_firepoint_indices[bank_to_fire].begin(); std::advance(middle_iterator, shuffle_start); - std::shuffle(middle_iterator, swp->primary_firepoint_indices[bank_to_fire].end(), std::mt19937(rd())); + std::shuffle(middle_iterator, swp->primary_firepoint_indices[bank_to_fire].end(), rng); std::rotate(swp->primary_firepoint_indices[bank_to_fire].begin(), middle_iterator, swp->primary_firepoint_indices[bank_to_fire].end()); break; } case FiringPattern::RANDOM_REPEATING: { - std::random_device rd; - std::shuffle(swp->primary_firepoint_indices[bank_to_fire].begin(), swp->primary_firepoint_indices[bank_to_fire].end(), std::mt19937(rd())); + std::mt19937 rng(util::Random::shuffle_seed()); + std::shuffle(swp->primary_firepoint_indices[bank_to_fire].begin(), swp->primary_firepoint_indices[bank_to_fire].end(), rng); break; } default: { diff --git a/code/utils/Random.cpp b/code/utils/Random.cpp index 2d944d281c6..df6c9a5e01b 100644 --- a/code/utils/Random.cpp +++ b/code/utils/Random.cpp @@ -7,6 +7,9 @@ #include #include "globalincs/pstypes.h" +#include "globalincs/systemvars.h" + +extern bool Cmdline_reuse_rng_seed; namespace util { @@ -54,6 +57,23 @@ void Random::seed(unsigned int val) SCP_rng.seed(val); } +void Random::seed_ranges(unsigned int val) +{ + Assert(val > 0); + seeder.seed(val); +} + +unsigned int Random::shuffle_seed() +{ + // multiplayer seeds the main RNG from the netgame and ignores -seed, so don't draw from it there + if (Cmdline_reuse_rng_seed && !(Game_mode & GM_MULTIPLAYER)) { + return static_cast(next()); + } + + std::random_device rd; + return rd(); +} + int Random::next() { return SCP_rng.next(); diff --git a/code/utils/Random.h b/code/utils/Random.h index 0ebb61655a6..be465531b51 100644 --- a/code/utils/Random.h +++ b/code/utils/Random.h @@ -10,6 +10,12 @@ class Random { // seed(1) will result in RNG using its default seed static void seed(unsigned int val); + // Seed independent RandomRange instances created after this call. + static void seed_ranges(unsigned int val); + + // Seed for a one-off generator handed to std::shuffle. With -seed (outside multiplayer) it is drawn + // from the main RNG, so shuffles repeat from run to run; otherwise it comes from the OS, as before. + static unsigned int shuffle_seed(); // return a value in [0, MAX_VALUE] static int next(); // uniformly between 0 and MAX_VALUE diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index be6ddf71d60..64530f5118b 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -1882,8 +1882,15 @@ void game_init() Game_current_mission_filename[0] = 0; - // seed the random number generator - Random::seed(static_cast(time(nullptr))); + // Seed both game RNG families from the command line when reproducible runs + // are requested. RandomRange instances must receive their seed before the + // tables are parsed during game initialization. + if (Cmdline_reuse_rng_seed) { + Random::seed(Cmdline_rng_seed); + Random::seed_ranges(Cmdline_rng_seed); + } else { + Random::seed(static_cast(time(nullptr))); + } ImGui::CreateContext();