From fa674ef3ae0184e6ef0915cd384c00ca86515f16 Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 13 Oct 2025 15:16:30 +0200 Subject: [PATCH 1/6] Make file writes atomic --- .../SPTarkov.Server.Core/Utils/FileUtil.cs | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs index b3f3e4ab..20b9ad61 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,45 @@ 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); } 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, 0, fileContent.Length); + await fs.FlushAsync(); + } + + File.Move(tempFilePath, filePath, overwrite: true); + } + catch + { + if (File.Exists(tempFilePath)) + { + try + { + File.Delete(tempFilePath); + } + catch { } + } + throw; + } } private void CreateFile(string filePath) From 3d1f757d1e7a15b7331a85ee663594c7a6093b90 Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 13 Oct 2025 15:19:49 +0200 Subject: [PATCH 2/6] Fix index going out of range on probability object array --- .../Utils/Collections/ProbabilityObjectArray.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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); } From 740895056f43356fb80cec6155878cd5761921ef Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 13 Oct 2025 15:23:39 +0200 Subject: [PATCH 3/6] Accept VS suggestion --- Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs index 20b9ad61..65eee9fa 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs @@ -123,7 +123,7 @@ public class FileUtil var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true) ) { - await fs.WriteAsync(fileContent, 0, fileContent.Length); + await fs.WriteAsync(fileContent); await fs.FlushAsync(); } From 6d65e68e29b42210fad7f47c138720b0a8e35377 Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 13 Oct 2025 15:34:46 +0200 Subject: [PATCH 4/6] Add comments --- Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs index 65eee9fa..8a961dbb 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/FileUtil.cs @@ -106,6 +106,10 @@ public class FileUtil 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) { var directoryPath = Path.GetDirectoryName(filePath); @@ -124,9 +128,12 @@ public class FileUtil ) { 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 From 4cf3279f97b1201949fb1f6c200fbb586ca61949 Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 13 Oct 2025 16:27:21 +0200 Subject: [PATCH 5/6] Add handler for PointsEarnedDuringSession overflow --- .../Models/Eft/Common/Tables/BotBase.cs | 1 + .../Json/Converters/SafeDoubleConverter.cs | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs 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/Json/Converters/SafeDoubleConverter.cs b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs new file mode 100644 index 00000000..04577710 --- /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 : double.MinValue; + } + 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); + } +} From a9b715c0c37df0a9807bcef02181542e1535da6f Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 13 Oct 2025 16:42:43 +0200 Subject: [PATCH 6/6] Set to 0 if we're below 0 --- .../Utils/Json/Converters/SafeDoubleConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs index 04577710..5a5ca511 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/SafeDoubleConverter.cs @@ -19,7 +19,7 @@ public class SafeDoubleConverter : JsonConverter try { var decimalValue = reader.GetDecimal(); - return decimalValue > 0 ? double.MaxValue : double.MinValue; + return decimalValue > 0 ? double.MaxValue : 0; } catch {