From 491525dd0996e1e0b32dba2bfcb27d6bec91c418 Mon Sep 17 00:00:00 2001 From: btawaifi Date: Thu, 15 Oct 2026 23:01:43 +0300 Subject: [PATCH] Make seeded runs reproducible across the RNG families the simulation uses -seed only seeded the main RNG. RandomRange instances (seeded from a separate generator before tables parse) and the std::random_device shuffles of weapon firepoints, asteroid splits and random-message senders stayed unseeded, so two runs with the same seed could diverge from the first frame. - Random::seed_ranges() seeds the generator that RandomRange instances draw from. - game_init() seeds both families when -seed is given, before table parsing. - Random::shuffle_seed() gives each std::shuffle its generator seed: a draw from the seeded main RNG under -seed, OS entropy otherwise (normal gameplay is unchanged). The ship firepoint, asteroid-split and message-sender shuffles use it instead of three separate std::random_device sites. In multiplayer the main RNG is seeded from the netgame and -seed is ignored, so the shuffles keep using OS entropy there and the shared sequence is unchanged. RandomRange instances are seeded once, when they are created (mostly while the tables are parsed), so a mission restarted in the same process does not repeat their draws. Reproducible runs need one process per run. --- code/asteroid/asteroid.cpp | 5 +++-- code/parse/sexp.cpp | 4 ++-- code/ship/ship.cpp | 28 ++++++++++++++-------------- code/utils/Random.cpp | 20 ++++++++++++++++++++ code/utils/Random.h | 6 ++++++ freespace2/freespace.cpp | 11 +++++++++-- 6 files changed, 54 insertions(+), 20 deletions(-) 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();