diff --git a/Libraries/Core/Generators/RepeatableQuestGenerator.cs b/Libraries/Core/Generators/RepeatableQuestGenerator.cs index 5ffb0e9b..26533f68 100644 --- a/Libraries/Core/Generators/RepeatableQuestGenerator.cs +++ b/Libraries/Core/Generators/RepeatableQuestGenerator.cs @@ -515,7 +515,7 @@ public class RepeatableQuestGenerator( ); // Be fair, don't var the items be more expensive than the reward - var multi = _randomUtil.GetFloat((float)0.5, 1); + var multi = _randomUtil.GetDouble(0.5, 1); var roublesBudget = Math.Floor( (double)(_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi) ); diff --git a/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs b/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs index cfffd78c..79f96ec6 100644 --- a/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs +++ b/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs @@ -264,7 +264,7 @@ public class RepeatableQuestRewardGenerator( return Math.Floor( effectiveDifficulty * _mathUtil.Interp1(pmcLevel, levelsConfig, xpConfig) * - _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ?? + _randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ?? 0 ); } @@ -276,7 +276,7 @@ public class RepeatableQuestRewardGenerator( return Math.Ceiling( effectiveDifficulty * _mathUtil.Interp1(pmcLevel, levelsConfig, gpCoinConfig) * - _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ?? + _randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ?? 0 ); } @@ -289,7 +289,7 @@ public class RepeatableQuestRewardGenerator( 100 * effectiveDifficulty * _mathUtil.Interp1(pmcLevel, levelsConfig, reputationConfig) * - _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ?? + _randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ?? 0 ) / 100; @@ -307,7 +307,7 @@ public class RepeatableQuestRewardGenerator( return Math.Floor( effectiveDifficulty * _mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * - _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ?? + _randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ?? 0 ); } diff --git a/Libraries/Core/Helpers/RepairHelper.cs b/Libraries/Core/Helpers/RepairHelper.cs index a1c87609..04c2af26 100644 --- a/Libraries/Core/Helpers/RepairHelper.cs +++ b/Libraries/Core/Helpers/RepairHelper.cs @@ -124,7 +124,7 @@ public class RepairHelper( ? armorMaterialSettings.MaxRepairKitDegradation : armorMaterialSettings.MaxRepairDegradation; - var duraLossPercent = _randomUtil.GetFloat((float)minMultiplier, (float)maxMultiplier); + var duraLossPercent = _randomUtil.GetDouble((double)minMultiplier, (double)maxMultiplier); var duraLossMultipliedByTraderMultiplier = duraLossPercent * armorMax * traderQualityMultipler; return Math.Round(duraLossMultipliedByTraderMultiplier, 2); @@ -153,7 +153,7 @@ public class RepairHelper( maxRepairDeg = itemProps.MaxRepairDegradation; } - var duraLossPercent = _randomUtil.GetFloat((float)minRepairDeg, (float)maxRepairDeg); + var duraLossPercent = _randomUtil.GetDouble((double)minRepairDeg, (double)maxRepairDeg); var duraLossMultipliedByTraderMultiplier = duraLossPercent * weaponMax * traderQualityMultipler; return Math.Round(duraLossMultipliedByTraderMultiplier, 2); diff --git a/Libraries/Core/Helpers/TraderHelper.cs b/Libraries/Core/Helpers/TraderHelper.cs index 387f30ac..0721608b 100644 --- a/Libraries/Core/Helpers/TraderHelper.cs +++ b/Libraries/Core/Helpers/TraderHelper.cs @@ -582,9 +582,7 @@ public class TraderHelper( var traderBuyBackPricePercent = traderBase.LoyaltyLevels.FirstOrDefault().BuyPriceCoefficient; var itemHandbookPrice = _handbookHelper.GetTemplatePrice(tpl); - var priceTraderBuysItemAt = Math.Round( - _randomUtil.GetPercentOfValue(traderBuyBackPricePercent ?? 0, itemHandbookPrice ?? 0) - ); + var priceTraderBuysItemAt = _randomUtil.GetPercentOfValue(traderBuyBackPricePercent ?? 0, itemHandbookPrice ?? 0, 0); // Price from this trader is higher than highest found, update if (priceTraderBuysItemAt > highestPrice) diff --git a/Libraries/Core/Services/CircleOfCultistService.cs b/Libraries/Core/Services/CircleOfCultistService.cs index 6b2ebba3..c8f66a1a 100644 --- a/Libraries/Core/Services/CircleOfCultistService.cs +++ b/Libraries/Core/Services/CircleOfCultistService.cs @@ -126,9 +126,9 @@ public class CircleOfCultistService( private double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings) { // Get a randomised value to multiply the sacrificed rouble cost by - var rewardAmountMultiplier = _randomUtil.GetFloat( - (float)cultistCircleSettings.RewardPriceMultiplerMinMax.Min, - (float)cultistCircleSettings.RewardPriceMultiplerMinMax.Max + var rewardAmountMultiplier = _randomUtil.GetDouble( + (double)cultistCircleSettings.RewardPriceMultiplerMinMax.Min, + (double)cultistCircleSettings.RewardPriceMultiplerMinMax.Max ); // Adjust value generated by the players hideout management skill diff --git a/Libraries/Core/Services/RepairService.cs b/Libraries/Core/Services/RepairService.cs index d012a00e..7a848c72 100644 --- a/Libraries/Core/Services/RepairService.cs +++ b/Libraries/Core/Services/RepairService.cs @@ -529,9 +529,7 @@ public class RepairService( Rarity = bonusRarityName, BuffType = bonusTypeName, Value = bonusValue, - ThresholdDurability = Math.Round( - _randomUtil.GetPercentOfValue(bonusThresholdPercent, item.Upd.Repairable.Durability.Value) - ) + ThresholdDurability = _randomUtil.GetPercentOfValue(bonusThresholdPercent, item.Upd.Repairable.Durability.Value, 0) }; } diff --git a/Libraries/Core/Utils/RandomUtil.cs b/Libraries/Core/Utils/RandomUtil.cs index c64c44fd..06a8cd9d 100644 --- a/Libraries/Core/Utils/RandomUtil.cs +++ b/Libraries/Core/Utils/RandomUtil.cs @@ -11,6 +11,8 @@ namespace Core.Utils; public class RandomUtil(ISptLogger _logger, ICloner _cloner) { public readonly Random Random = new(); + private const int DecimalPointRandomPrecision = 6; + private static readonly int DecimalPointRandomPrecisionMultiplier = (int) Math.Pow(10, DecimalPointRandomPrecision); /// /// The IEEE-754 standard for double-precision floating-point numbers limits the number of digits (including both @@ -22,40 +24,20 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// Generates a random integer between the specified minimum and maximum values, inclusive. /// /// The minimum value (inclusive). - /// The maximum value (inclusive). + /// The maximum value (optional). + /// If max is exclusive or not. /// A random integer between the specified minimum and maximum values. - public int GetInt(int min, int max) + public int GetInt(int min, int max = int.MaxValue, bool exclusive = false) { // Prevents a potential integer overflow. - if (max == int.MaxValue) max -= 1; + if (exclusive && max == int.MaxValue) + { + max -= 1; + } - // maxVal is exclusive of the passed value, so add 1 - return max > min ? Random.Next(min, max + 1) : min; + return max > min ? Random.Next(min, exclusive ? max : max + 1) : min; } - - /// - /// Generates a random integer between 1 (inclusive) and the specified maximum value (exclusive). - /// If the maximum value is less than or equal to 1, it returns 1. - /// - /// The upper bound (exclusive) for the random integer generation. - /// A random integer between 1 and max - 1, or 1 if max is less than or equal to 1. - public int GetIntEx(int max) - { - return max > 2 ? Random.Next(1, max - 1) : 1; - } - - /// - /// Generates a random floating-point number within the specified range ~6-9 digits (4 bytes). - /// - /// The minimum value of the range (inclusive). - /// The maximum value of the range (exclusive). - /// A random floating-point number between `min` (inclusive) and `max` (exclusive). - public float GetFloat(float min, float max) - { - return (float)GetSecureRandomNumber() * (max - min) + min; - } - - + /// /// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes). /// @@ -64,7 +46,13 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// A random floating-point number between `min` (inclusive) and `max` (exclusive). public double GetDouble(double min, double max) { - return GetSecureRandomNumber() * (max - min) + min; + var realMin = (long) (min * DecimalPointRandomPrecisionMultiplier); + var realMax = (long) (max * DecimalPointRandomPrecisionMultiplier); + + return Math.Round( + Random.NextInt64(realMin, realMax) / (double)DecimalPointRandomPrecisionMultiplier, + DecimalPointRandomPrecision + ); } /// @@ -73,7 +61,7 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// A random boolean value, where the probability of `true` and `false` is approximately equal. public bool GetBool() { - return GetSecureRandomNumber() < 0.5; + return Random.Next(0, 2) == 1; } /// @@ -83,11 +71,11 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// The number to calculate the percentage of. /// The number of decimal places to round the result to (default is 2). /// The calculated percentage of the given number, rounded to the specified number of decimal places. - public float GetPercentOfValue(double percent, double number, int toFixed = 2) + public double GetPercentOfValue(double percent, double number, int toFixed = 2) { var num = percent * number / 100; - return (float)Math.Round(num, toFixed); + return Math.Round(num, toFixed); } /// @@ -110,9 +98,9 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// `true` if the event occurs, `false` otherwise. public bool GetChance100(double? chancePercent) { - chancePercent = Math.Clamp(chancePercent ?? 0, 0f, 100f); + chancePercent = Math.Clamp(chancePercent ?? 0, 0D, 100D); - return GetIntEx(100) <= chancePercent; + return GetInt(0, 100) <= chancePercent; } /// @@ -196,7 +184,7 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) // Check if the generated value is valid if (valueDrawn < 0) return attempt > 100 - ? GetDouble(0.01f, mean * 2f) + ? GetDouble(0.01D, mean * 2D) : GetNormallyDistributedRandomNumber(mean, sigma, attempt + 1); return valueDrawn; @@ -229,42 +217,20 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// and MaxSignificantDigits(15), inclusive. If not provided, precision is determined by the input values. /// /// - public double RandNum(double val1, double val2 = 0, byte? precision = null) + public double RandNum(double val1, double val2 = 0, int precision = DecimalPointRandomPrecision) { if (!double.IsFinite(val1) || !double.IsFinite(val2)) throw new ArgumentException("RandNum() parameters 'value1' and 'value2' must be finite numbers."); // Determine the range var min = Math.Min(val1, val2); var max = Math.Max(val1, val2); - - // Validate and adjust precision - if (precision is not null) - { - if (precision > MaxSignificantDigits) - throw new ArgumentOutOfRangeException( - nameof(precision), "Must be less than 16"); - - // Calculate the number of whole-number digits in the maximum absolute value of the range - var maxAbsoluteValue = Math.Max(Math.Abs(min), Math.Abs(max)); - var wholeNumberDigits = (int)Math.Floor(Math.Log10(maxAbsoluteValue)) + 1; - - var maxAllowedPrecision = Math.Max(0, MaxSignificantDigits - wholeNumberDigits); - - if (precision > maxAllowedPrecision) - throw new ArgumentException( - $"RandNum() precision of {precision} exceeds the allowable precision ({maxAllowedPrecision}) for the given values." - ); - } - - var result = GetSecureRandomNumber() * (max - min) + min; - - // Determine effective precision - var maxPrecision = Math.Max(GetNumberPrecision(val1), GetNumberPrecision(val2)); - var effectivePrecision = precision ?? maxPrecision; - - var factor = Math.Pow(2, effectivePrecision); - - return Math.Round(result * factor) / factor; + + var realPrecision = (long) Math.Pow(10, precision); + + var minInt = (long) (min * realPrecision); + var maxInt = (long) (max * realPrecision); + + return Math.Round(Random.NextInt64(minInt, maxInt) / (double) realPrecision, precision); } /// @@ -436,23 +402,9 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// produce a floating-point number in the range [0, 1). /// /// A secure random number between 0 (inclusive) and 1 (exclusive). - private static double GetSecureRandomNumber() + private double GetSecureRandomNumber() { - var buffer = new byte[6]; // 48 bits - using (var rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(buffer); - } - - // Convert byte array to unsigned long - ulong value = 0; - for (var i = 0; i < buffer.Length; i++) - { - value |= (ulong)buffer[i] << (8 * (buffer.Length - 1 - i)); - } - - const ulong maxInteger = 281474976710656; // 2^48 - return (double)value / maxInteger; + return Random.NextSingle(); } /// @@ -462,11 +414,13 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) /// The number of decimal places, or 0 if none exist. public int GetNumberPrecision(double num) { - var parts = num.ToString($"G{MaxSignificantDigits}").Split('.'); - - return parts.Length > 1 - ? parts[1].Length - : 0; + var factor = 0; + while (num % 1 > double.Epsilon) + { + num *= 10D; + factor++; + } + return factor; } public T? GetArrayValue(IEnumerable list) diff --git a/UnitTests/Tests/Test.cs b/UnitTests/Tests/Test.cs index 72e3b20d..0b88ab2a 100644 --- a/UnitTests/Tests/Test.cs +++ b/UnitTests/Tests/Test.cs @@ -14,7 +14,7 @@ public class Test public void Setup() { - var importer = new ImporterUtil(new MockLogger(), new FileUtil(), new JsonUtil()); + var importer = new ImporterUtil(new MockLogger(), new FileUtil(new MockLogger()), new JsonUtil()); var loadTask = importer.LoadRecursiveAsync("./TestAssets/"); loadTask.Wait(); _templates = loadTask.Result; diff --git a/UnitTests/Tests/Utils/RandomUtilTests.cs b/UnitTests/Tests/Utils/RandomUtilTests.cs index 170d18a9..2a52e541 100644 --- a/UnitTests/Tests/Utils/RandomUtilTests.cs +++ b/UnitTests/Tests/Utils/RandomUtilTests.cs @@ -12,8 +12,8 @@ public sealed class RandomUtilTests [TestMethod] public void GetIntTest() { - // Run 100 test cases - for (var i = 0; i < 100; i++) + // Run 10000 test cases + for (var i = 0; i < 10000; i++) { var result = _randomUtil.GetInt(0, 10); @@ -27,10 +27,10 @@ public sealed class RandomUtilTests [TestMethod] public void GetIntExTest() { - // Run 100 test cases - for (var i = 0; i < 100; i++) + // Run 10000 test cases + for (var i = 0; i < 10000; i++) { - var result = _randomUtil.GetIntEx(10); + var result = _randomUtil.GetInt(1, 10, true); if (result < 1 || result > 9) { @@ -40,16 +40,16 @@ public sealed class RandomUtilTests } [TestMethod] - public void GetFloatTest() + public void GetDoubleTest() { - // Run 100 test cases - for (var i = 0; i < 100; i++) + // Run 10000 test cases + for (var i = 0; i < 10000; i++) { - var result = _randomUtil.GetFloat(0f, 10f); + var result = _randomUtil.GetDouble(0D, 10D); - if (result < 0f || result >= 9f) + if (result is < 0d or >= 10d) { - Assert.Fail($"GetFloat(0f, 10f) out of range. Expected range [0.0f, 9.999f] but was {result}."); + Assert.Fail($"GetDouble(0d, 10d) out of range. Expected range [0.0d, 9.999d] but was {result}."); } } } @@ -135,11 +135,11 @@ public sealed class RandomUtilTests [TestMethod] public void RandNumTest() { - for (var i = 0; i < 100; i++) + for (var i = 0; i < 10000; i++) { - var result = _randomUtil.RandNum(0, 10); + var result = _randomUtil.RandNum(0, 10, 15); - if (result < 0 || result > 9) + if (result < 0 || result >= 10) { Assert.Fail($"RandNum(0, 10) out of range. Expected range [0, 9.999d] but was {result}."); } @@ -150,11 +150,11 @@ public sealed class RandomUtilTests } } - for (var i = 0; i < 100; i++) + for (var i = 0; i < 10000; i++) { var result = _randomUtil.RandNum(10); - if (result < 0 || result > 9) + if (result < 0 || result >= 10) { Assert.Fail($"RandNum(10) out of range. Expected range [0, 9.999d] but was {result}."); } @@ -182,4 +182,15 @@ public sealed class RandomUtilTests result.SequenceEqual(orig), $"Shuffle test failed. Expected: {string.Join(", ", orig)}, but got {string.Join(", ", result)}"); } + + [TestMethod] + [DataRow(0.1, 1)] + [DataRow(0.0001, 4)] + [DataRow(0, 0)] + [DataRow(10000000, 0)] + [DataRow(0.000_000_000_000_1D, 13)] + public void GetNumberPrecision_WithDoubles_ReturnsDecimalPoints(double value, int decimalPoints) + { + Assert.AreEqual(decimalPoints, _randomUtil.GetNumberPrecision(value)); + } }