From 77ba9d957a40eb719b7faec3772da66511abd730 Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Thu, 19 Feb 2026 11:49:46 +0000 Subject: [PATCH] Engine: General updates to xirand - Clears up definitions and selections of different random engines - All engines go through the same get and auto-seed routine, using a 4k initial seed sequence. - All engines are guaranteed to be properly seeded when accessed from any thread - Tidied up the API for xirand, and added a new weighted selection API - Added Squirrel5 Random Engine (thanks for the recommendation JMC!) - Added many comments and explanations - Verified that other game server implementations also use MT/MT64/SIMD variants of MT, etc. Co-Authored-By: Jasson McMorris --- src/common/engine.cpp | 2 - src/common/rng/mersennetwister.h | 75 --------- src/common/rng/mersennetwister64.h | 75 --------- src/common/rng/null.h | 49 +----- src/common/rng/pcg.h | 87 ---------- src/common/rng/pcg64.h | 84 ---------- src/common/rng/squirrel5.h | 96 +++++++++++ src/common/xirand.cpp | 56 ++++++- src/common/xirand.h | 252 +++++++++++++++++++---------- src/map/daily_system.cpp | 21 +-- 10 files changed, 329 insertions(+), 468 deletions(-) delete mode 100644 src/common/rng/mersennetwister.h delete mode 100644 src/common/rng/mersennetwister64.h delete mode 100644 src/common/rng/pcg.h delete mode 100644 src/common/rng/pcg64.h create mode 100644 src/common/rng/squirrel5.h diff --git a/src/common/engine.cpp b/src/common/engine.cpp index f43104b6572..345abe41add 100644 --- a/src/common/engine.cpp +++ b/src/common/engine.cpp @@ -26,8 +26,6 @@ Engine::Engine() { - srand(earth_time::timestamp()); - xirand::seed(); } Engine::~Engine() diff --git a/src/common/rng/mersennetwister.h b/src/common/rng/mersennetwister.h deleted file mode 100644 index 86b2d018b3e..00000000000 --- a/src/common/rng/mersennetwister.h +++ /dev/null @@ -1,75 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2023 LandSandBoat Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#pragma once - -#include "common/logging.h" - -#include -#include - -// -// Forward declare sysrandom which is built in the xirand.h/cpp compilation unit -// - -extern size_t sysrandom(void* dst, size_t dstlen); - -class xirand -{ -public: - static std::mt19937& rng() - { - static thread_local std::mt19937 e{}; - return e; - } - - static void seed() - { - ShowInfo("Seeding Mersenne Twister 32 bit RNG"); - - uint32_t seed; - sysrandom(&seed, sizeof(seed)); - - rng().seed(seed); - } - - // - // Declarations for RNG methods implemented in xirand.h. - // - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline T GetRandomNumber(T max); - - template - static inline typename T::value_type GetRandomElement(T* container); - - template - static inline typename T::value_type GetRandomElement(T& container); - - template - static inline T GetRandomElement(std::initializer_list list); -}; diff --git a/src/common/rng/mersennetwister64.h b/src/common/rng/mersennetwister64.h deleted file mode 100644 index 4268f774cfe..00000000000 --- a/src/common/rng/mersennetwister64.h +++ /dev/null @@ -1,75 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2023 LandSandBoat Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#pragma once - -#include "common/logging.h" - -#include -#include - -// -// Forward declare sysrandom which is built in the xirand.h/cpp compilation unit -// - -extern size_t sysrandom(void* dst, size_t dstlen); - -class xirand -{ -public: - static std::mt19937_64& rng() - { - static thread_local std::mt19937_64 e{}; - return e; - } - - static void seed() - { - ShowInfo("Seeding Mersenne Twister 64 bit RNG"); - - uint64_t seed; - sysrandom(&seed, sizeof(seed)); - - rng().seed(seed); - } - - // - // Declarations for RNG methods implemented in xirand.h. - // - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline T GetRandomNumber(T max); - - template - static inline typename T::value_type GetRandomElement(T* container); - - template - static inline typename T::value_type GetRandomElement(T& container); - - template - static inline T GetRandomElement(std::initializer_list list); -}; diff --git a/src/common/rng/null.h b/src/common/rng/null.h index 205c0d8d72d..db196b645a8 100644 --- a/src/common/rng/null.h +++ b/src/common/rng/null.h @@ -21,17 +21,10 @@ #pragma once -#include "common/logging.h" - -#include +#include +#include #include -// -// Forward declare sysrandom which is built in the xirand.h/cpp compilation unit -// - -extern size_t sysrandom(void* dst, size_t dstlen); - class NullRandomEngine { public: @@ -55,43 +48,9 @@ class NullRandomEngine void seed(result_type) { } -}; -class xirand -{ -public: - static NullRandomEngine& rng() + template + void seed(Sseq&) { - static thread_local NullRandomEngine e{}; - return e; } - - static void seed() - { - ShowInfo("Seeding Null Random Engine (does nothing)"); - - rng().seed(42); - } - - // - // Declarations for RNG methods implemented in xirand.h. - // - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline T GetRandomNumber(T max); - - template - static inline typename T::value_type GetRandomElement(T* container); - - template - static inline typename T::value_type GetRandomElement(T& container); - - template - static inline T GetRandomElement(std::initializer_list list); }; diff --git a/src/common/rng/pcg.h b/src/common/rng/pcg.h deleted file mode 100644 index 698d11da34e..00000000000 --- a/src/common/rng/pcg.h +++ /dev/null @@ -1,87 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2023 LandSandBoat Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#pragma once - -#include "common/logging.h" - -// https://github.com/imneme/pcg-cpp -#include "pcg_random.hpp" - -#include -#include - -// -// Forward declare sysrandom which is built in the xirand.h/cpp compilation unit -// - -extern size_t sysrandom(void* dst, size_t dstlen); - -class xirand -{ -public: - static pcg32& rng() - { - static thread_local pcg32 e{}; - return e; - } - - static void seed() - { - ShowInfo("Seeding PCG32 RNG"); - - std::array seed_data; // PCG states seem to be bit size * 2 * 2, here 64 bit numbers * 2 - - pcg32::state_type seed; - - for (auto it = seed_data.begin(); it != seed_data.end(); ++it) - { - sysrandom(&seed, sizeof(seed)); - - *it = seed; - } - - std::seed_seq seq(seed_data.cbegin(), seed_data.cend()); - rng().seed(seq); - } - - // - // Declarations for RNG methods implemented in xirand.h. - // - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline T GetRandomNumber(T max); - - template - static inline typename T::value_type GetRandomElement(T* container); - - template - static inline typename T::value_type GetRandomElement(T& container); - - template - static inline T GetRandomElement(std::initializer_list list); -}; diff --git a/src/common/rng/pcg64.h b/src/common/rng/pcg64.h deleted file mode 100644 index 1871354ba46..00000000000 --- a/src/common/rng/pcg64.h +++ /dev/null @@ -1,84 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2023 LandSandBoat Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#pragma once - -#include "common/logging.h" - -// https://github.com/imneme/pcg-cpp -#include "pcg_random.hpp" - -#include -#include - -// Forward declare sysrandom which is built in the xirand.h/cpp compilation unit -extern size_t sysrandom(void* dst, size_t dstlen); - -class xirand -{ -public: - static pcg64& rng() - { - static thread_local pcg64 e{}; - return e; - } - - static void seed() - { - ShowInfo("Seeding PCG64 RNG"); - - std::array seed_data; - - pcg64::state_type seed; - - for (auto it = seed_data.begin(); it != seed_data.end(); ++it) - { - sysrandom(&seed, sizeof(seed)); - - *it = seed; - } - - std::seed_seq seq(seed_data.cbegin(), seed_data.cend()); - rng().seed(seq); - } - - // - // Declarations for RNG methods implemented in xirand.h. - // - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline T GetRandomNumber(T max); - - template - static inline typename T::value_type GetRandomElement(T* container); - - template - static inline typename T::value_type GetRandomElement(T& container); - - template - static inline T GetRandomElement(std::initializer_list list); -}; diff --git a/src/common/rng/squirrel5.h b/src/common/rng/squirrel5.h new file mode 100644 index 00000000000..9c8fd8fc1e8 --- /dev/null +++ b/src/common/rng/squirrel5.h @@ -0,0 +1,96 @@ +/* +=========================================================================== + + Copyright (c) 2025 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include +#include +#include + +// ATTR: Squirrel Eiserloh https://www.gdcvault.com/play/1024365/Math-for-Game-Programmers-Noise +// Original Source: http://eiserloh.net/noise/SquirrelNoise5.hpp +class Squirrel5 +{ +public: + using result_type = uint32_t; + + static constexpr result_type min() + { + return 0; + } + + static constexpr result_type max() + { + return std::numeric_limits::max(); + } + + Squirrel5(result_type seed_val = 0) + : m_seed(seed_val) + { + } + + void seed(result_type seed_val) + { + m_seed = seed_val; + m_position = 0; + } + + template + void seed(Sseq& seq) + { + std::array buf; + seq.generate(buf.begin(), buf.end()); + m_seed = buf[0]; + m_position = 0; + } + + result_type operator()() + { + return noise(m_position++, m_seed); + } + + static uint32_t noise(int32_t position, uint32_t seed) + { + constexpr uint32_t SQ5_BIT_NOISE1 = 0xd2a80a3f; // 11010010101010000000101000111111 + constexpr uint32_t SQ5_BIT_NOISE2 = 0xa884f197; // 10101000100001001111000110010111 + constexpr uint32_t SQ5_BIT_NOISE3 = 0x6C736F4B; // 01101100011100110110111101001011 + constexpr uint32_t SQ5_BIT_NOISE4 = 0xB79F3ABB; // 10110111100111110011101010111011 + constexpr uint32_t SQ5_BIT_NOISE5 = 0x1b56c4f5; // 00011011010101101100010011110101 + + uint32_t mangled = static_cast(position); + mangled *= SQ5_BIT_NOISE1; + mangled += seed; + mangled ^= (mangled >> 9); + mangled += SQ5_BIT_NOISE2; + mangled ^= (mangled >> 11); + mangled *= SQ5_BIT_NOISE3; + mangled ^= (mangled >> 13); + mangled += SQ5_BIT_NOISE4; + mangled ^= (mangled >> 15); + mangled *= SQ5_BIT_NOISE5; + mangled ^= (mangled >> 17); + return mangled; + } + +private: + uint32_t m_seed = 0; + uint32_t m_position = 0; +}; diff --git a/src/common/xirand.cpp b/src/common/xirand.cpp index 9681ccd87af..10d5880c72d 100644 --- a/src/common/xirand.cpp +++ b/src/common/xirand.cpp @@ -1,4 +1,4 @@ -/* +/* =========================================================================== Copyright (c) 2025 LandSandBoat Dev Teams @@ -19,8 +19,12 @@ =========================================================================== */ +#include "xirand.h" + +#include +#include #include -#include +#include // https://stackoverflow.com/a/45069417 #ifdef _WIN32 @@ -30,7 +34,7 @@ #include -bool acquire_context(HCRYPTPROV* ctx) +auto acquire_context(HCRYPTPROV* ctx) -> bool { if (!CryptAcquireContext(ctx, nullptr, nullptr, PROV_RSA_FULL, 0)) { @@ -40,7 +44,7 @@ bool acquire_context(HCRYPTPROV* ctx) return true; } -size_t sysrandom(void* dst, size_t dstlen) +auto sysrandom(void* dst, size_t dstlen) -> size_t { HCRYPTPROV ctx; if (!acquire_context(&ctx)) @@ -67,7 +71,7 @@ size_t sysrandom(void* dst, size_t dstlen) #include #include -size_t sysrandom(void* dst, size_t dstlen) +auto sysrandom(void* dst, size_t dstlen) -> size_t { int bytes = syscall(SYS_getrandom, dst, dstlen, 0); if (bytes != dstlen) @@ -78,7 +82,7 @@ size_t sysrandom(void* dst, size_t dstlen) return dstlen; } #else // OSX -size_t sysrandom(void* dst, size_t dstlen) +auto sysrandom(void* dst, size_t dstlen) -> size_t { char* buffer = reinterpret_cast(dst); std::ifstream stream("/dev/urandom", std::ios_base::binary | std::ios_base::in); @@ -87,3 +91,43 @@ size_t sysrandom(void* dst, size_t dstlen) return dstlen; } #endif + +void xirand::seed() +{ + // 4096 bits of entropy (128 * 32-bit words) + // + // Mersenne Twister engines (std::mt19937 and std::mt19937_64) have a large internal state + // (~2.5KB). Seeding with a single 32-bit value leaves the state sparsely populated, which + // can lead to statistical artifacts in the first outputs. Using a larger pool of entropy + // combined with std::seed_seq ensures the state is thoroughly mixed. + // + // While engines like PCG or Squirrel5 require much less state, providing this amount of + // entropy is inexpensive (once per thread) and ensures a high-quality start for any + // selected engine. This is overkill, but cheap to do once per thread anyway. + std::array seedData{}; + sysrandom(seedData.data(), seedData.size() * sizeof(uint32_t)); + + std::seed_seq seq(seedData.begin(), seedData.end()); + rng().seed(seq); +} + +auto xirand::rng() -> SelectedRandomEngine& +{ + // We use thread_local to give every thread its own independent RNG instance. + // This avoids the need for locks which would otherwise cause significant + // contention in a multi-threaded environment. + // + // The static 'initialized' flag ensures that every new thread automatically + // seeds itself exactly once upon its first request for a random number. + + static thread_local bool initialized{ false }; + static thread_local SelectedRandomEngine engine{}; + + if (!initialized) + { + initialized = true; + seed(); + } + + return engine; +} diff --git a/src/common/xirand.h b/src/common/xirand.h index 8fd310053c6..e48a11a93ef 100644 --- a/src/common/xirand.h +++ b/src/common/xirand.h @@ -1,4 +1,4 @@ -/* +/* =========================================================================== Copyright (c) 2010-2015 Darkstar Dev Teams @@ -22,127 +22,211 @@ #pragma once -// -// You can choose an RNG by commenting/uncommenting one of the lines below. -// The default is Mersenne Twister in 64 bit. -// - -// TODO: Make these selectable with #ifdef build flags - -// #include "rng/null.h" -// #include "rng/mersennetwister.h" -#include "rng/mersennetwister64.h" -// #include "rng/pcg.h" -// #include "rng/pcg64.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -// RNG template is as follows: -// The RNG must comply with the CPP standard UniformRandomBitGenerator ( see https://en.cppreference.com/w/cpp/named_req/UniformRandomBitGenerator ) // -// The RNG must return a static thread local instance of the RNG with "rng()" as the accessor, such as: -// static std::mt19937& rng() +// @brief Random Engine Selection // -// The RNG must have a seed function declared as follows: +// Define one of the following macros to select a specific RNG engine. +// If none are defined, XIRAND_MT64 (std::mt19937_64) is used by default. // -// static void seed() +// - XIRAND_PCG64 +// - XIRAND_PCG32 +// - XIRAND_MT32 +// - XIRAND_MT64 +// - XIRAND_SQUIRREL5 +// - XIRAND_NULL // -// The RNG must have the following boilerplate templated function declarations -/* - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline typename std::enable_if::value, T>::type GetRandomNumber(T min, T max); - - template - static inline T GetRandomNumber(T max); - - template - static inline typename T::value_type GetRandomElement(T* container); - - template - static inline typename T::value_type GetRandomElement(T& container); - template - static inline T GetRandomElement(std::initializer_list list); -*/ -// See the latest revision of src/common/RNG/mersennetwister.h for an example. +#if defined(XIRAND_PCG64) +#include +using SelectedRandomEngine = pcg64; +#elif defined(XIRAND_PCG32) +#include +using SelectedRandomEngine = pcg32; +#elif defined(XIRAND_MT32) +using SelectedRandomEngine = std::mt19937; +#elif defined(XIRAND_SQUIRREL5) +#include "rng/squirrel5.h" +using SelectedRandomEngine = Squirrel5; +#elif defined(XIRAND_NULL) +#include "rng/null.h" +using SelectedRandomEngine = NullRandomEngine; +#else +using SelectedRandomEngine = std::mt19937_64; +#endif + +static_assert(std::uniform_random_bit_generator, "SelectedRandomEngine must satisfy the UniformRandomBitGenerator concept"); +static_assert(std::is_default_constructible_v, "SelectedRandomEngine must be default constructible"); + +/// @brief Cross-platform implementation of secure random numbers for seeding. +auto sysrandom(void* dst, size_t dstlen) -> size_t; + +namespace xirand +{ +/// @brief Accessor for the thread-local engine. It is guaranteed to be correctly seeded. +[[nodiscard]] auto rng() -> SelectedRandomEngine&; + +/// @brief Manually seed the thread-local RNG. +/// @note Calls to rng() will automatically call seed(). You don't need to do this manually. +/// We _do_ manually call this in xi_test to force different seeds. +void seed(); + +/// @brief Generates a random number in the half-open interval [min, max). +/// @tparam T Integer type. +/// @param min Minimum value (inclusive). +/// @param max Maximum value (exclusive). +/// @return Random value in [min, max). +/// @note max is subtracted by one as per an inconsistency in the standard, see +/// https://bugs.llvm.org/show_bug.cgi?id=18767#c1 +/// This change results in both real and integer templates having the same min/max range. +template +[[nodiscard]] inline auto GetRandomNumber(T min, T max) -> T; + +template +[[nodiscard]] inline auto GetRandomNumber(T min, T max) -> T; + +/// @brief Generates a random number in the half-open interval [0, max). +/// @tparam T Number type. +/// @param max Maximum value (exclusive). +/// @return Random value in [0, max). +template +[[nodiscard]] inline auto GetRandomNumber(T max) -> T; + +/// @brief Gets a random element from the given random_access_range (e.g. vector, array, deque). +/// @tparam R Random access range type. +/// @param range The container or range. +/// @return Copy of the randomly selected element (or value-initialized T if empty). +/// @throws std::out_of_range if the range is empty and T is not default constructible. +template +[[nodiscard]] inline auto GetRandomElement(R&& range) -> std::ranges::range_value_t; + +/// @brief Returns a random index based on weights. +/// @param weights Span of weights. +/// @return Index of the selected weight. +[[nodiscard]] inline auto GetWeightedIndex(std::span weights) -> size_t; + +/// @brief Returns a random index based on weights. +/// @param weights Initializer list of weights. +/// @return Index of the selected weight. +/// @example GetWeightedIndex({70, 20, 10}) has a 70% chance to return 0. +[[nodiscard]] inline auto GetWeightedIndex(std::initializer_list weights) -> size_t; + +/// @brief Returns a random element from a map of . +/// @tparam Container Map-like container type. +/// @param table Map of elements to weights. +/// @return Key of the selected element. +/// @example GetWeightedElement(std::map{{"Common", 70}, {"Rare", 30}}) +/// @throws std::out_of_range if the table is empty and key_type is not default constructible. +template +[[nodiscard]] inline auto GetWeightedElement(Container const& table) -> typename Container::key_type; +} // namespace xirand // -// The following functions are not meant to be edited by the end user. Change at your own risk -- you will receive no support. +// inline impls // -// Generates a random number in the half-open interval [min, max) -// @param min -// @param max -// @returns result -// -// Do note that max is subtracted by one as per an inconsistency in the standard, see -// https://bugs.llvm.org/show_bug.cgi?id=18767#c1 -// this change results in both real and integer templates having the same min/max range -template -inline typename std::enable_if::value, T>::type xirand::GetRandomNumber(T min, T max) +template +[[nodiscard]] inline auto xirand::GetRandomNumber(T min, T max) -> T { - if (min == max - 1 || max == min || min > max) + if (min >= max) { return min; } + std::uniform_int_distribution dist(min, max - 1); return dist(rng()); } -template -inline typename std::enable_if::value, T>::type xirand::GetRandomNumber(T min, T max) +template +[[nodiscard]] inline auto xirand::GetRandomNumber(T min, T max) -> T { if (min >= max) { return min; } + std::uniform_real_distribution dist(min, max); return dist(rng()); } -// Generates a random number in the half-open interval [0, max) -// @param min -// @param max -// @returns result -// -// Do note that max is subtracted by one as per an inconsistency in the standard, see -// https://bugs.llvm.org/show_bug.cgi?id=18767#c1 -// this change results in both real and integer templates having the same min/max range template -inline T xirand::GetRandomNumber(T max) +[[nodiscard]] inline auto xirand::GetRandomNumber(T max) -> T { return GetRandomNumber(0, max); } -// Gets a random element from the given stl-like container (container must have members: at() and size()). -// @param container -// @returns result -template -inline typename T::value_type xirand::GetRandomElement(T* container) +template +[[nodiscard]] inline auto xirand::GetRandomElement(R&& range) -> std::ranges::range_value_t { - // NOTE: the specialisation for integral types uses: dist(min, max - 1), so no need to offset container->size() - return container->at(GetRandomNumber(0U, container->size())); + if (std::ranges::empty(range)) + { + if constexpr (std::is_default_constructible_v>) + { + return {}; + } + else + { + throw std::out_of_range("GetRandomElement called on empty container with non-default-constructible type"); + } + } + + auto index = GetRandomNumber>(0, std::ranges::size(range)); + return range[index]; } -// Gets a random element from the given stl-like container (container must have members: at() and size()). -// @param container -// @returns result -template -inline typename T::value_type xirand::GetRandomElement(T& container) +[[nodiscard]] inline auto xirand::GetWeightedIndex(std::span weights) -> size_t { - return GetRandomElement(&container); + if (weights.empty()) + { + return 0; + } + + std::discrete_distribution dist(weights.begin(), weights.end()); + return dist(rng()); } -// Gets a random element from the given initializer_list. -// @param initializer_list -// @returns result -template -inline T xirand::GetRandomElement(std::initializer_list list) +[[nodiscard]] inline auto xirand::GetWeightedIndex(std::initializer_list weights) -> size_t { - std::vector container(list); - return GetRandomElement(container); + return GetWeightedIndex(std::span(weights)); } -// Get secure random numbers -size_t sysrandom(void* dst, size_t dstlen); +template +[[nodiscard]] inline auto xirand::GetWeightedElement(Container const& table) -> typename Container::key_type +{ + if (table.empty()) + { + if constexpr (std::is_default_constructible_v) + { + return {}; + } + else + { + throw std::out_of_range("GetWeightedElement called on empty container with non-default-constructible type"); + } + } + + std::vector weights; + std::vector elements; + weights.reserve(table.size()); + elements.reserve(table.size()); + + for (auto const& [item, weight] : table) + { + elements.push_back(item); + weights.push_back(static_cast(weight)); + } + + std::discrete_distribution dist(weights.begin(), weights.end()); + return elements[dist(rng())]; +} diff --git a/src/map/daily_system.cpp b/src/map/daily_system.cpp index 622cdea423d..b0ef76b1b91 100644 --- a/src/map/daily_system.cpp +++ b/src/map/daily_system.cpp @@ -48,47 +48,48 @@ std::vector gobbieJunk = { uint16 SelectItem(CCharEntity* player, uint8 dial) { - std::vector* dialItems = &gobbieJunk; + std::reference_wrapper> dialItems = gobbieJunk; + switch (dial) { case 1: { - dialItems = &materialsDialItems; + dialItems = materialsDialItems; break; } case 2: { - dialItems = &foodDialItems; + dialItems = foodDialItems; break; } case 3: { - dialItems = &medicineDialItems; + dialItems = medicineDialItems; break; } case 4: { - dialItems = &sundries1DialItems; + dialItems = sundries1DialItems; break; } case 5: { - dialItems = &sundries2DialItems; + dialItems = sundries2DialItems; break; } case 6: { - dialItems = &specialDialItems; + dialItems = specialDialItems; break; } } - uint16 selection = xirand::GetRandomElement(dialItems); + uint16 selection = xirand::GetRandomElement(dialItems.get()); // Check if Rare item is already owned and substitute with Goblin trash item. if ((itemutils::GetItem(selection)->getFlag() & ITEM_FLAG_RARE) > 0 && charutils::HasItem(player, selection)) { - dialItems = &gobbieJunk; - selection = xirand::GetRandomElement(dialItems); + dialItems = gobbieJunk; + selection = xirand::GetRandomElement(dialItems.get()); } return selection;