Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Cj
2025-10-13 20:52:30 -04:00
4 changed files with 97 additions and 15 deletions
@@ -425,6 +425,7 @@ public record MasterySkill
public record CommonSkill
{
[JsonConverter(typeof(SafeDoubleConverter))]
public double PointsEarnedDuringSession { get; set; }
public long LastAccess { get; set; }
@@ -159,7 +159,13 @@ public class ProbabilityObjectArray<K, V> : List<ProbabilityObject<K, V>>
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);
}
@@ -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);
}
/// <summary>
/// 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.
/// </summary>
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)
@@ -0,0 +1,49 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SPTarkov.Server.Core.Utils.Json.Converters;
public class SafeDoubleConverter : JsonConverter<double>
{
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);
}
}