diff --git a/ASFFreeGames.Tests/ASFFreeGames.Tests.csproj b/ASFFreeGames.Tests/ASFFreeGames.Tests.csproj
index 97ad63d..b00c897 100644
--- a/ASFFreeGames.Tests/ASFFreeGames.Tests.csproj
+++ b/ASFFreeGames.Tests/ASFFreeGames.Tests.csproj
@@ -4,6 +4,8 @@
enable
false
+
+ net8.0
diff --git a/ASFFreeGames.Tests/RandomUtilsTests.cs b/ASFFreeGames.Tests/RandomUtilsTests.cs
index 7222f09..5398497 100644
--- a/ASFFreeGames.Tests/RandomUtilsTests.cs
+++ b/ASFFreeGames.Tests/RandomUtilsTests.cs
@@ -23,7 +23,7 @@ public static TheoryData GetTestData() =>
[SuppressMessage("ReSharper", "InconsistentNaming")]
public void NextGaussian_Should_Have_Expected_Mean_And_Std(double mean, double standardDeviation, int sampleSize, double marginOfError) {
// Arrange
- using RandomUtils.GaussianRandom rng = new();
+ RandomUtils.GaussianRandom rng = new();
// Act
// Generate a large number of samples from the normal distribution
diff --git a/ASFFreeGames.Tests/Reddit/RedditHelperTests.cs b/ASFFreeGames.Tests/Reddit/RedditHelperTests.cs
index ed37a32..061c0df 100644
--- a/ASFFreeGames.Tests/Reddit/RedditHelperTests.cs
+++ b/ASFFreeGames.Tests/Reddit/RedditHelperTests.cs
@@ -1,36 +1,37 @@
using System;
+using System.Buffers;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Threading;
+using System.Threading.Tasks;
using Maxisoft.ASF.Reddit;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
+using Maxisoft.Utils.Collections.Spans;
using Xunit;
-namespace ASFFreeGames.Tests.Reddit;
+namespace Maxisoft.ASF.Tests.Reddit;
public sealed class RedditHelperTests {
- private static readonly Lazy ASFinfo = new(LoadAsfinfoJson);
-
[Fact]
- public void TestNotEmpty() {
- JToken payload = ASFinfo.Value;
- RedditGameEntry[] entries = RedditHelper.LoadMessages(payload.Value("data")!["children"]!);
+ public async Task TestNotEmpty() {
+ RedditGameEntry[] entries = await LoadAsfinfoEntries().ConfigureAwait(false);
Assert.NotEmpty(entries);
}
[Theory]
[InlineData("s/762440")]
[InlineData("a/1601550")]
- public void TestContains(string appid) {
- JToken payload = ASFinfo.Value;
- RedditGameEntry[] entries = RedditHelper.LoadMessages(payload.Value("data")!["children"]!);
+ public async Task TestContains(string appid) {
+ RedditGameEntry[] entries = await LoadAsfinfoEntries().ConfigureAwait(false);
Assert.Contains(new RedditGameEntry(appid, default(ERedditGameEntryKind), long.MaxValue), entries, new GameEntryIdentifierEqualityComparer());
}
[Fact]
- public void TestMaintainOrder() {
- JToken payload = ASFinfo.Value;
- RedditGameEntry[] entries = RedditHelper.LoadMessages(payload.Value("data")!["children"]!);
+ public async Task TestMaintainOrder() {
+ RedditGameEntry[] entries = await LoadAsfinfoEntries().ConfigureAwait(false);
int app762440 = Array.FindIndex(entries, static entry => entry.Identifier == "s/762440");
int app1601550 = Array.FindIndex(entries, static entry => entry.Identifier == "a/1601550");
Assert.InRange(app762440, 0, long.MaxValue);
@@ -42,9 +43,8 @@ public void TestMaintainOrder() {
}
[Fact]
- public void TestFreeToPlayParsing() {
- JToken payload = ASFinfo.Value;
- RedditGameEntry[] entries = RedditHelper.LoadMessages(payload.Value("data")!["children"]!);
+ public async Task TestFreeToPlayParsing() {
+ RedditGameEntry[] entries = await LoadAsfinfoEntries().ConfigureAwait(false);
RedditGameEntry f2pEntry = Array.Find(entries, static entry => entry.Identifier == "a/1631250");
Assert.True(f2pEntry.IsFreeToPlay);
@@ -70,9 +70,8 @@ public void TestFreeToPlayParsing() {
}
[Fact]
- public void TestDlcParsing() {
- JToken payload = ASFinfo.Value;
- RedditGameEntry[] entries = RedditHelper.LoadMessages(payload.Value("data")!["children"]!);
+ public async Task TestDlcParsing() {
+ RedditGameEntry[] entries = await LoadAsfinfoEntries().ConfigureAwait(false);
RedditGameEntry f2pEntry = Array.Find(entries, static entry => entry.Identifier == "a/1631250");
Assert.False(f2pEntry.IsForDlc);
@@ -97,14 +96,18 @@ public void TestDlcParsing() {
Assert.False(paidEntry.IsForDlc);
}
- private static JToken LoadAsfinfoJson() {
+ private static async Task LoadAsfinfoEntries() {
Assembly assembly = Assembly.GetExecutingAssembly();
- using Stream stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.ASFinfo.json")!;
+ await using Stream stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.ASFinfo.json")!;
+ JsonNode jsonNode = await JsonNode.ParseAsync(stream).ConfigureAwait(false) ?? JsonNode.Parse("{}")!;
+
+ return RedditHelper.LoadMessages(jsonNode["data"]?["children"]!);
+ }
- using StreamReader reader = new(stream);
- using JsonTextReader jsonTextReader = new(reader);
+ private static async Task ReadToEndAsync(Stream stream, CancellationToken cancellationToken) {
+ using StreamReader reader = new StreamReader(stream);
- return JToken.Load(jsonTextReader);
+ return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
}
}
diff --git a/ASFFreeGames.sln.DotSettings b/ASFFreeGames.sln.DotSettings
index eaf19de..760550c 100644
--- a/ASFFreeGames.sln.DotSettings
+++ b/ASFFreeGames.sln.DotSettings
@@ -715,6 +715,10 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
<Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
True
OnlyMarkers
@@ -761,6 +765,7 @@
True
True
True
+ True
True
True
True
diff --git a/ASFFreeGames/ASFFreeGames.csproj b/ASFFreeGames/ASFFreeGames.csproj
index 58140e4..c9fa9d1 100644
--- a/ASFFreeGames/ASFFreeGames.csproj
+++ b/ASFFreeGames/ASFFreeGames.csproj
@@ -4,6 +4,7 @@
true
True
pdbonly
+ net8.0
@@ -11,7 +12,6 @@
-
diff --git a/ASFFreeGames/ASFFreeGamesPlugin.cs b/ASFFreeGames/ASFFreeGamesPlugin.cs
index 3493d2c..8482ccc 100644
--- a/ASFFreeGames/ASFFreeGamesPlugin.cs
+++ b/ASFFreeGames/ASFFreeGamesPlugin.cs
@@ -2,15 +2,16 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using ASFFreeGames.Commands;
+using ASFFreeGames.Configurations;
using JetBrains.Annotations;
using Maxisoft.ASF.Configurations;
-using Newtonsoft.Json.Linq;
using SteamKit2;
using static ArchiSteamFarm.Core.ASF;
@@ -24,7 +25,6 @@ internal interface IASFFreeGamesPlugin {
}
#pragma warning disable CA1812 // ASF uses this class during runtime
-[UsedImplicitly]
[SuppressMessage("Design", "CA1001:Disposable fields")]
internal sealed class ASFFreeGamesPlugin : IASF, IBot, IBotConnection, IBotCommand2, IUpdateAware, IASFFreeGamesPlugin {
internal const string StaticName = nameof(ASFFreeGamesPlugin);
@@ -62,12 +62,6 @@ public ASFFreeGamesPlugin() {
_context.Value = new PluginContext(Bots, BotContextRegistry, Options, LoggerFilter) { CancellationTokenLazy = new Lazy(() => CancellationTokenSourceLazy.Value.Token) };
}
- public async Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) {
- ASFFreeGamesOptionsLoader.Bind(ref OptionsField);
- Options.VerboseLog ??= GlobalDatabase?.LoadFromJsonStorage($"{Name}.Verbose")?.ToObject() ?? Options.VerboseLog;
- await SaveOptions(CancellationToken).ConfigureAwait(false);
- }
-
public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
if (!Context.Valid) {
CreateContext();
@@ -92,6 +86,17 @@ public Task OnLoaded() {
return Task.CompletedTask;
}
+ public async Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) {
+ ASFFreeGamesOptionsLoader.Bind(ref OptionsField);
+ JsonElement? jsonElement = GlobalDatabase?.LoadFromJsonStorage($"{Name}.Verbose");
+
+ if (jsonElement?.ValueKind is JsonValueKind.True) {
+ Options.VerboseLog = true;
+ }
+
+ await SaveOptions(CancellationToken).ConfigureAwait(false);
+ }
+
public async Task OnUpdateFinished(Version currentVersion, Version newVersion) => await SaveOptions(Context.CancellationToken).ConfigureAwait(false);
public Task OnUpdateProceeding(Version currentVersion, Version newVersion) => Task.CompletedTask;
diff --git a/ASFFreeGames/BloomFilters/StringBloomFilterSpan.cs b/ASFFreeGames/BloomFilters/StringBloomFilterSpan.cs
index 97c0a1f..da695f8 100644
--- a/ASFFreeGames/BloomFilters/StringBloomFilterSpan.cs
+++ b/ASFFreeGames/BloomFilters/StringBloomFilterSpan.cs
@@ -43,7 +43,7 @@ public StringBloomFilterSpan(BitSpan bitSpan, int k = 1) {
/// Adds a new item to the filter. It cannot be removed.
///
/// The item.
- public void Add([JetBrains.Annotations.NotNull] in string item) {
+ public void Add(in string item) {
// start flipping bits for each hash of item
#pragma warning disable CA1062
int primaryHash = item.GetHashCode(StringComparison.Ordinal);
@@ -61,7 +61,7 @@ public void Add([JetBrains.Annotations.NotNull] in string item) {
///
/// The item.
/// The .
- public bool Contains([JetBrains.Annotations.NotNull] in string item) {
+ public bool Contains(in string item) {
#pragma warning disable CA1062
int primaryHash = item.GetHashCode(StringComparison.Ordinal);
#pragma warning restore CA1062
diff --git a/ASFFreeGames/Commands/CommandDispatcher.cs b/ASFFreeGames/Commands/CommandDispatcher.cs
index a1d6679..491ac83 100644
--- a/ASFFreeGames/Commands/CommandDispatcher.cs
+++ b/ASFFreeGames/Commands/CommandDispatcher.cs
@@ -3,6 +3,8 @@
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
+using ASFFreeGames.Commands.GetIp;
+using ASFFreeGames.Configurations;
using Maxisoft.ASF;
namespace ASFFreeGames.Commands {
diff --git a/ASFFreeGames/Commands/FreeGamesCommand.cs b/ASFFreeGames/Commands/FreeGamesCommand.cs
index d3372c3..6d6b5f1 100644
--- a/ASFFreeGames/Commands/FreeGamesCommand.cs
+++ b/ASFFreeGames/Commands/FreeGamesCommand.cs
@@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
+using ASFFreeGames.Configurations;
using Maxisoft.ASF;
using Maxisoft.ASF.Configurations;
using Maxisoft.ASF.Reddit;
@@ -190,7 +191,7 @@ private async Task CollectGames(IEnumerable bots, ECollectGameRequestS
int res = 0;
try {
- ICollection games = await RedditHelper.GetGames().ConfigureAwait(false);
+ ICollection games = await RedditHelper.GetGames(cancellationToken).ConfigureAwait(false);
LogNewGameCount(games, VerboseLog || requestSource is ECollectGameRequestSource.RequestedByUser);
diff --git a/ASFFreeGames/Commands/GetIPCommand.cs b/ASFFreeGames/Commands/GetIp/GetIPCommand.cs
similarity index 65%
rename from ASFFreeGames/Commands/GetIPCommand.cs
rename to ASFFreeGames/Commands/GetIp/GetIPCommand.cs
index 8419944..fb14572 100644
--- a/ASFFreeGames/Commands/GetIPCommand.cs
+++ b/ASFFreeGames/Commands/GetIp/GetIPCommand.cs
@@ -1,18 +1,18 @@
using System;
using System.Globalization;
using System.IO;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
-using Maxisoft.ASF;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
+using JsonSerializer = System.Text.Json.JsonSerializer;
-namespace ASFFreeGames.Commands;
+namespace ASFFreeGames.Commands.GetIp;
+// ReSharper disable once ClassNeverInstantiated.Local
internal sealed class GetIPCommand : IBotCommand {
private const string GetIPAddressUrl = "https://httpbin.org/ip";
@@ -28,8 +28,12 @@ internal sealed class GetIPCommand : IBotCommand {
}
try {
- ObjectResponse? result = await web.UrlGetToJsonObject(new Uri(GetIPAddressUrl)).ConfigureAwait(false);
- string origin = result?.Content?.Value("origin") ?? "";
+ await using StreamResponse? result = await web.UrlGetToStream(new Uri(GetIPAddressUrl), cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (result?.Content is null) { return null; }
+
+ GetIpReponse? reponse = await JsonSerializer.DeserializeAsync(result.Content, cancellationToken: cancellationToken).ConfigureAwait(false);
+ string? origin = reponse?.Origin;
if (!string.IsNullOrWhiteSpace(origin)) {
return IBotCommand.FormatBotResponse(bot, origin);
diff --git a/ASFFreeGames/Commands/GetIp/GetIpReponse.cs b/ASFFreeGames/Commands/GetIp/GetIpReponse.cs
new file mode 100644
index 0000000..3d556ce
--- /dev/null
+++ b/ASFFreeGames/Commands/GetIp/GetIpReponse.cs
@@ -0,0 +1,3 @@
+namespace ASFFreeGames.Commands.GetIp;
+
+internal record GetIpReponse(string Origin) { }
diff --git a/ASFFreeGames/Commands/GetIp/GetIpReponseContext.cs b/ASFFreeGames/Commands/GetIp/GetIpReponseContext.cs
new file mode 100644
index 0000000..94e0083
--- /dev/null
+++ b/ASFFreeGames/Commands/GetIp/GetIpReponseContext.cs
@@ -0,0 +1,7 @@
+using System.Text.Json.Serialization;
+
+namespace ASFFreeGames.Commands.GetIp;
+
+//[JsonSourceGenerationOptions(WriteIndented = false, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip)]
+//[JsonSerializable(typeof(GetIpReponse))]
+//internal partial class GetIpReponseContext : JsonSerializerContext { }
diff --git a/ASFFreeGames/Configurations/ASFFreeGamesOptions.cs b/ASFFreeGames/Configurations/ASFFreeGamesOptions.cs
index bebd9b7..733c020 100644
--- a/ASFFreeGames/Configurations/ASFFreeGamesOptions.cs
+++ b/ASFFreeGames/Configurations/ASFFreeGamesOptions.cs
@@ -2,44 +2,46 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam;
using Maxisoft.ASF;
-using Newtonsoft.Json;
-namespace Maxisoft.ASF {
- public class ASFFreeGamesOptions {
- // Use TimeSpan instead of long for representing time intervals
- [JsonProperty("recheckInterval")]
- public TimeSpan RecheckInterval { get; set; } = TimeSpan.FromMinutes(30);
+namespace ASFFreeGames.Configurations;
- // Use Nullable instead of bool? for nullable value types
- [JsonProperty("randomizeRecheckInterval")]
- public Nullable RandomizeRecheckInterval { get; set; }
+public class ASFFreeGamesOptions {
+ // Use TimeSpan instead of long for representing time intervals
+ [JsonPropertyName("recheckInterval")]
+ public TimeSpan RecheckInterval { get; set; } = TimeSpan.FromMinutes(30);
- [JsonProperty("skipFreeToPlay")]
- public Nullable SkipFreeToPlay { get; set; }
+ // Use Nullable instead of bool? for nullable value types
+ [JsonPropertyName("randomizeRecheckInterval")]
+ public bool? RandomizeRecheckInterval { get; set; }
- // ReSharper disable once InconsistentNaming
- [JsonProperty("skipDLC")]
- public Nullable SkipDLC { get; set; }
+ [JsonPropertyName("skipFreeToPlay")]
+ public bool? SkipFreeToPlay { get; set; }
- // Use IReadOnlyCollection instead of HashSet for blacklist property
- [JsonProperty("blacklist")]
- public IReadOnlyCollection Blacklist { get; set; } = new HashSet();
+ // ReSharper disable once InconsistentNaming
+ [JsonPropertyName("skipDLC")]
+ public bool? SkipDLC { get; set; }
- [JsonProperty("verboseLog")]
- public Nullable VerboseLog { get; set; }
+ // Use IReadOnlyCollection instead of HashSet for blacklist property
+ [JsonPropertyName("blacklist")]
+ public IReadOnlyCollection Blacklist { get; set; } = new HashSet();
- #region IsBlacklisted
- public bool IsBlacklisted(in GameIdentifier gid) {
- if (Blacklist.Count <= 0) {
- return false;
- }
+ [JsonPropertyName("verboseLog")]
+ public bool? VerboseLog { get; set; }
- return Blacklist.Contains(gid.ToString()) || Blacklist.Contains(gid.Id.ToString(NumberFormatInfo.InvariantInfo));
+ #region IsBlacklisted
+ public bool IsBlacklisted(in GameIdentifier gid) {
+ if (Blacklist.Count <= 0) {
+ return false;
}
- public bool IsBlacklisted(in Bot? bot) => bot is null || ((Blacklist.Count > 0) && Blacklist.Contains($"bot/{bot.BotName}"));
- #endregion
+ return Blacklist.Contains(gid.ToString()) || Blacklist.Contains(gid.Id.ToString(CultureInfo.InvariantCulture));
}
+
+ public bool IsBlacklisted(in Bot? bot) => bot is null || ((Blacklist.Count > 0) && Blacklist.Contains($"bot/{bot.BotName}"));
+ #endregion
}
+
+
diff --git a/ASFFreeGames/Configurations/ASFFreeGamesOptionsContext.cs b/ASFFreeGames/Configurations/ASFFreeGamesOptionsContext.cs
new file mode 100644
index 0000000..0a35785
--- /dev/null
+++ b/ASFFreeGames/Configurations/ASFFreeGamesOptionsContext.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+using ASFFreeGames.Configurations;
+
+namespace Maxisoft.ASF.Configurations;
+
+//[JsonSourceGenerationOptions(WriteIndented = false, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+//[JsonSerializable(typeof(ASFFreeGamesOptions))]
+//internal partial class ASFFreeGamesOptionsContext : JsonSerializerContext { }
diff --git a/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs b/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs
index 947c4ae..ec6d076 100644
--- a/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs
+++ b/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs
@@ -5,6 +5,8 @@
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm;
+using ASFFreeGames.Commands.GetIp;
+using ASFFreeGames.Configurations;
using Microsoft.Extensions.Configuration;
namespace Maxisoft.ASF.Configurations;
@@ -58,12 +60,7 @@ public static async Task Save(ASFFreeGamesOptions options, CancellationToken can
#pragma warning restore CAC001
// Use JsonSerializerOptions.PropertyNamingPolicy to specify the JSON property naming convention
- await JsonSerializer.SerializeAsync(
- fs, options, new JsonSerializerOptions {
- WriteIndented = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- }, cancellationToken
- ).ConfigureAwait(false);
+ await JsonSerializer.SerializeAsync(fs, options, cancellationToken: cancellationToken).ConfigureAwait(false);
}
finally {
Semaphore.Release();
diff --git a/ASFFreeGames/GameIdentifierParser.cs b/ASFFreeGames/GameIdentifierParser.cs
index 801cfa7..d831af5 100644
--- a/ASFFreeGames/GameIdentifierParser.cs
+++ b/ASFFreeGames/GameIdentifierParser.cs
@@ -13,7 +13,7 @@ internal static class GameIdentifierParser {
/// The query string to parse.
/// The resulting game identifier if the parsing was successful.
/// True if the parsing was successful; otherwise, false.
- public static bool TryParse([NotNull] ReadOnlySpan query, out GameIdentifier result) {
+ public static bool TryParse(ReadOnlySpan query, out GameIdentifier result) {
if (query.IsEmpty) // Check for empty query first
{
result = default(GameIdentifier);
diff --git a/ASFFreeGames/LoggerFilter.cs b/ASFFreeGames/LoggerFilter.cs
index 45b5bc0..a9b53b3 100644
--- a/ASFFreeGames/LoggerFilter.cs
+++ b/ASFFreeGames/LoggerFilter.cs
@@ -124,16 +124,10 @@ private static Logger GetLogger(ArchiLogger logger, string name = "ASF") {
private bool RemoveFilters(BotName botName) => Filters.TryRemove(botName, out _);
// A class that implements a disposable object for removing filters
- private sealed class LoggerRemoveFilterDisposable : IDisposable {
- private readonly LinkedListNode> Node;
-
- public LoggerRemoveFilterDisposable(LinkedListNode> node) => Node = node;
-
- public void Dispose() => Node.List?.Remove(Node);
+ private sealed class LoggerRemoveFilterDisposable(LinkedListNode> node) : IDisposable {
+ public void Dispose() => node.List?.Remove(node);
}
// A class that implements a custom filter that invokes a method
- private class MarkedWhenMethodFilter : WhenMethodFilter {
- public MarkedWhenMethodFilter(Func filterMethod) : base(filterMethod) { }
- }
+ private class MarkedWhenMethodFilter(Func filterMethod) : WhenMethodFilter(filterMethod);
}
diff --git a/ASFFreeGames/PluginContext.cs b/ASFFreeGames/PluginContext.cs
index 5684e42..f9e6631 100644
--- a/ASFFreeGames/PluginContext.cs
+++ b/ASFFreeGames/PluginContext.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading;
using ArchiSteamFarm.Steam;
+using ASFFreeGames.Configurations;
namespace Maxisoft.ASF;
diff --git a/ASFFreeGames/RandomUtils.cs b/ASFFreeGames/RandomUtils.cs
index e3c48a0..ac9f713 100644
--- a/ASFFreeGames/RandomUtils.cs
+++ b/ASFFreeGames/RandomUtils.cs
@@ -11,7 +11,8 @@ namespace Maxisoft.ASF;
#nullable enable
public static class RandomUtils {
- internal sealed class GaussianRandom : RandomNumberGenerator {
+ internal sealed class GaussianRandom {
+
// A flag to indicate if there is a stored value for the next Gaussian number
private int HasNextGaussian;
@@ -21,28 +22,43 @@ internal sealed class GaussianRandom : RandomNumberGenerator {
// The stored value for the next Gaussian number
private double NextGaussianValue;
- public override void GetBytes(byte[] data) => Fill(data);
+ private void GetNonZeroBytes(Span data) {
+ Span bytes = stackalloc byte[sizeof(long)];
+
+ static void fill(Span bytes) {
+ // use this method to use a RNGs function that's still included with the ASF trimmed binary
+ // do not try to refactor or optimize this without testing
+ byte[] rng = RandomNumberGenerator.GetBytes(bytes.Length);
+ ((ReadOnlySpan) rng).CopyTo(bytes);
+ }
- public override void GetNonZeroBytes(Span data) {
- Fill(data);
- Span buffer = stackalloc byte[1];
+ fill(bytes);
+ int c = 0;
for (int i = 0; i < data.Length; i++) {
- while (data[i] == default(byte)) {
- Fill(buffer);
- data[i] = buffer[0];
- }
+ byte value;
+
+ do {
+ value = bytes[c];
+ c++;
+
+ if (c >= bytes.Length) {
+ fill(bytes);
+ c = 0;
+ }
+ } while (value == 0);
+
+ data[i] = value;
}
}
- public override void GetNonZeroBytes(byte[] data) => GetNonZeroBytes((Span) data);
-
private double NextDouble() {
if (Interlocked.CompareExchange(ref HasNextGaussian, False, True) == True) {
return NextGaussianValue;
}
Span bytes = stackalloc byte[2 * sizeof(long)];
+
Span ulongs = MemoryMarshal.Cast(bytes);
double u1;
diff --git a/ASFFreeGames/Reddit/RedditHelper.cs b/ASFFreeGames/Reddit/RedditHelper.cs
index 323a06d..884c3a3 100644
--- a/ASFFreeGames/Reddit/RedditHelper.cs
+++ b/ASFFreeGames/Reddit/RedditHelper.cs
@@ -2,31 +2,34 @@
using System.Buffers;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Text.Json; // Not using System.Text.Json for JsonDocument
+using System.Text.Json.Nodes; // Using System.Text.Json.Nodes for JsonNode
using System.Text.RegularExpressions;
+using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
+using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using BloomFilter;
using Maxisoft.Utils.Collections.Spans;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
namespace Maxisoft.ASF.Reddit;
internal sealed partial class RedditHelper {
private const int BloomFilterBufferSize = 8;
-
private const int PoolMaxGameEntry = 1024;
private const string User = "ASFinfo";
private static readonly ArrayPool ArrayPool = ArrayPool.Create(PoolMaxGameEntry, 1);
- /// A method that gets a collection of Reddit game entries from a JSON object
///
/// Gets a collection of Reddit game entries from a JSON object.
///
/// A collection of Reddit game entries.
- public static async ValueTask> GetGames() {
+ public static async ValueTask> GetGames(CancellationToken cancellationToken) {
WebBrowser? webBrowser = ArchiSteamFarm.Core.ASF.WebBrowser;
RedditGameEntry[] result = Array.Empty();
@@ -34,36 +37,35 @@ public static async ValueTask> GetGames() {
return result;
}
- ObjectResponse? jsonPayload = null;
+ JsonNode jsonPayload;
try {
- jsonPayload = await TryGetPayload(webBrowser).ConfigureAwait(false);
+ ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo("b4 the payload");
+ jsonPayload = await GetPayload(webBrowser, cancellationToken).ConfigureAwait(false) ?? JsonNode.Parse("{}")!;
+ ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"got the payload");
}
catch (Exception exception) when (exception is JsonException or IOException) {
return result;
}
- if (jsonPayload is null) {
- return result;
+ try {
+ if ((jsonPayload["kind"]?.GetValue() != "Listing") ||
+ jsonPayload["data"] is null) {
+ return result;
+ }
}
+ catch (Exception e) when (e is FormatException or InvalidOperationException) {
+ ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo("invalid json");
- // Use pattern matching to check for null and type
- if (jsonPayload.Content is JObject jObject &&
- jObject.TryGetValue("kind", out JToken? kind) &&
- (kind.Value() == "Listing") &&
- jObject.TryGetValue("data", out JToken? data) &&
- data is JObject) {
- JToken? children = data["children"];
-
- if (children is not null) {
- return LoadMessages(children);
- }
+ return result;
}
- return result; // Return early if children is not found or not an array
+ JsonNode? childrenElement = jsonPayload["data"]?["children"];
+
+ return childrenElement is null ? result : LoadMessages(childrenElement);
}
- internal static RedditGameEntry[] LoadMessages(JToken children) {
+ internal static RedditGameEntry[] LoadMessages(JsonNode children) {
Span bloomFilterBuffer = stackalloc long[BloomFilterBufferSize];
StringBloomFilterSpan bloomFilter = new(bloomFilterBuffer, 3);
RedditGameEntry[] buffer = ArrayPool.Rent(PoolMaxGameEntry / 2);
@@ -71,10 +73,34 @@ internal static RedditGameEntry[] LoadMessages(JToken children) {
try {
SpanList list = new(buffer);
- foreach (JObject comment in children.Children()) {
- JToken? commentData = comment.GetValue("data", StringComparison.InvariantCulture);
- string text = commentData?.Value("body") ?? string.Empty;
- long date = commentData?.Value("created_utc") ?? commentData?.Value("created") ?? 0;
+ // ReSharper disable once LoopCanBePartlyConvertedToQuery
+ foreach (JsonNode? comment in children.AsArray()) {
+ JsonNode? commentData = comment?["data"];
+
+ if (commentData is null) {
+ continue;
+ }
+
+ long date;
+ string text;
+
+ try {
+ text = commentData["body"]?.GetValue() ?? string.Empty;
+
+ date = checked((long) (commentData["created_utc"]?.GetValue() ?? 0));
+
+ if (!double.IsNormal(date)) {
+ date = checked((long) (commentData["created"]?.GetValue() ?? 0));
+ }
+ }
+ catch (Exception e) when (e is FormatException or InvalidOperationException) {
+ continue;
+ }
+
+ if (!double.IsNormal(date) || (date <= 0)) {
+ continue;
+ }
+
MatchCollection matches = CommandRegex().Matches(text);
foreach (Match match in matches) {
@@ -95,7 +121,6 @@ internal static RedditGameEntry[] LoadMessages(JToken children) {
foreach (Capture capture in matchGroup.Captures) {
RedditGameEntry gameEntry = new(capture.Value, kind, date);
-
int index = -1;
if (bloomFilter.Contains(gameEntry.Identifier)) {
@@ -125,7 +150,6 @@ internal static RedditGameEntry[] LoadMessages(JToken children) {
return list.ToArray();
}
finally {
- // Use a finally block to ensure that the buffer is returned to the pool
ArrayPool.Return(buffer);
}
}
@@ -145,24 +169,48 @@ internal static RedditGameEntry[] LoadMessages(JToken children) {
/// Tries to get a JSON object from Reddit.
///
/// The web browser instance to use.
+ ///
/// A JSON object response or null if failed.
/// Thrown when Reddit returns a server error.
/// This method is based on this GitHub issue: https://github.com/maxisoft/ASFFreeGames/issues/28
- private static async Task?> TryGetPayload(WebBrowser webBrowser) {
+ private static async ValueTask GetPayload(WebBrowser webBrowser, CancellationToken cancellationToken) {
+ StreamResponse? stream = null;
+
try {
- return await webBrowser.UrlGetToJsonObject(GetUrl(), rateLimitingDelay: 500).ConfigureAwait(false);
- }
+ stream = await webBrowser.UrlGetToStream(GetUrl(), rateLimitingDelay: 500, cancellationToken: cancellationToken).ConfigureAwait(false);
- catch (JsonReaderException) {
- // ReSharper disable once UseAwaitUsing
- using StreamResponse? response = await webBrowser.UrlGetToStream(GetUrl(), rateLimitingDelay: 500).ConfigureAwait(false);
+ if (stream?.Content is null) {
+ throw new RedditServerException("Reddit server error: content is null", stream?.StatusCode ?? HttpStatusCode.InternalServerError);
+ }
- if (response is not null && response.StatusCode.IsServerErrorCode()) {
- throw new RedditServerException($"Reddit server error: {response.StatusCode}", response.StatusCode);
+ return await ParseJsonNode(stream, cancellationToken).ConfigureAwait(false);
+ }
+ catch (JsonException) {
+ if (stream is not null && stream.StatusCode.IsServerErrorCode()) {
+ throw new RedditServerException($"Reddit server error: {stream.StatusCode}", stream.StatusCode);
}
// If no RedditServerException was thrown, re-throw the original JsonReaderException
throw;
}
+ finally {
+ if (stream is not null) {
+ await stream.DisposeAsync().ConfigureAwait(false);
+ }
+
+ stream = null;
+ }
+ }
+
+ ///
+ /// Parses a JSON object from a stream response. Using not straightforward for ASF trimmed compatibility reasons
+ ///
+ /// The stream response containing the JSON data.
+ /// The cancellation token.
+ /// The parsed JSON object, or null if parsing fails.
+ private static async Task ParseJsonNode(StreamResponse stream, CancellationToken cancellationToken) {
+ using StreamReader reader = new(stream.Content!, Encoding.UTF8);
+
+ return JsonNode.Parse(await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false));
}
}
diff --git a/ArchiSteamFarm b/ArchiSteamFarm
index 113e0c9..efb7262 160000
--- a/ArchiSteamFarm
+++ b/ArchiSteamFarm
@@ -1 +1 @@
-Subproject commit 113e0c9b3c5758ebb04fa1c4a3cac5fd006730fc
+Subproject commit efb726211381a781da086415a6414ae3038d98bd
diff --git a/Directory.Build.props b/Directory.Build.props
index ab75cfc..1c398af 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
ASFFreeGames
1.4.0.0
- net7.0
+ net8.0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a4d766d..90ed91c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,7 +1,7 @@
-
+