diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs index ec9efa64..dd601592 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs @@ -425,6 +425,7 @@ public record MasterySkill public record CommonSkill { + [JsonConverter(typeof(SafeDoubleConverter))] public double PointsEarnedDuringSession { get; set; } public long LastAccess { get; set; } diff --git a/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs b/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs index 670de92c..3ef05bfe 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs @@ -159,7 +159,13 @@ public class ProbabilityObjectArray : List> for (var i = 0; i < itemCountToDraw; i++) { var rand = Random.Shared.NextDouble(); - var randomIndex = cumulativeProbabilities.FindIndex(probability => probability > rand); + var randomIndex = cumulativeProbabilities.FindIndex(probability => probability >= rand); + + if (randomIndex == -1) + { + continue; + } + results.Add(this[randomIndex].Key); } diff --git a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs index b3f3e4ab..8a961dbb 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs @@ -1,3 +1,4 @@ +using System.Text; using SPTarkov.DI.Annotations; namespace SPTarkov.Server.Core.Utils; @@ -101,27 +102,52 @@ public class FileUtil public async Task WriteFileAsync(string filePath, string fileContent) { - if (!DirectoryExists(Path.GetDirectoryName(filePath))) - { - CreateDirectory(Path.GetDirectoryName(filePath)); - } - - if (!FileExists(filePath)) - { - CreateFile(filePath); - } - - await File.WriteAllTextAsync(filePath, fileContent); + var bytes = Encoding.UTF8.GetBytes(fileContent); + await WriteFileAsync(filePath, bytes); } + /// + /// Writes a file atomically by first writing to a temporary file, then replacing the original. + /// This prevents corruption if the write operation fails or is interrupted. + /// public async Task WriteFileAsync(string filePath, byte[] fileContent) { - if (!FileExists(filePath)) + var directoryPath = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) { - CreateFile(filePath); + Directory.CreateDirectory(directoryPath); } - await File.WriteAllBytesAsync(filePath, fileContent); + var tempFilePath = filePath + ".bak"; + + try + { + await using ( + var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true) + ) + { + await fs.WriteAsync(fileContent); + + // We flush here so we can be sure it's immediately committed to disk + await fs.FlushAsync(); + } + + // Overwrite over the old file + File.Move(tempFilePath, filePath, overwrite: true); + } + catch + { + if (File.Exists(tempFilePath)) + { + try + { + File.Delete(tempFilePath); + } + catch { } + } + throw; + } } private void CreateFile(string filePath) diff --git a/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs new file mode 100644 index 00000000..5a5ca511 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SPTarkov.Server.Core.Utils.Json.Converters; + +public class SafeDoubleConverter : JsonConverter +{ + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Number: + try + { + return reader.GetDouble(); + } + catch (FormatException) + { + try + { + var decimalValue = reader.GetDecimal(); + return decimalValue > 0 ? double.MaxValue : 0; + } + catch + { + return double.MaxValue; + } + } + + case JsonTokenType.String: + if (double.TryParse(reader.GetString(), out var stringParsed)) + { + return stringParsed; + } + return 0; + + case JsonTokenType.Null: + return 0; + + default: + return 0; + } + } + + public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } +}