diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f5bd967..4b89ffe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,8 +5,8 @@ on: [push, pull_request]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
- DOTNET_SDK_VERSION: 7.0.x
- DOTNET_FRAMEWORK: net7.0
+ DOTNET_SDK_VERSION: 8.0.x
+ DOTNET_FRAMEWORK: net8.0
jobs:
main:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 6d6b042..7b3c6a0 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,8 +6,8 @@ env:
CONFIGURATION: Release
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
- DOTNET_SDK_VERSION: 7.0.x
- NET_CORE_VERSION: net7.0
+ DOTNET_SDK_VERSION: 8.0.x
+ NET_CORE_VERSION: net8.0
NET_FRAMEWORK_VERSION: net48
PLUGIN_NAME: ASFFreeGames
diff --git a/.github/workflows/test_integration.yml b/.github/workflows/test_integration.yml
index 1d8f885..a5fc2e2 100644
--- a/.github/workflows/test_integration.yml
+++ b/.github/workflows/test_integration.yml
@@ -11,7 +11,7 @@ on:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
- DOTNET_SDK_VERSION: 7.0.x
+ DOTNET_SDK_VERSION: 8.0.x
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
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..49b1f6f 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,20 @@ 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")!;
+#pragma warning disable CA2007
+ await using Stream stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.ASFinfo.json")!;
+#pragma warning restore CA2007
+ JsonNode jsonNode = await JsonNode.ParseAsync(stream).ConfigureAwait(false) ?? JsonNode.Parse("{}")!;
+
+ return RedditHelper.LoadMessages(jsonNode["data"]?["children"]!);
+ }
+ private static async Task ReadToEndAsync(Stream stream, CancellationToken cancellationToken) {
using StreamReader reader = new(stream);
- using JsonTextReader jsonTextReader = new(reader);
- 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..cd96e28 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);
@@ -54,7 +54,7 @@ internal static PluginContext Context {
public ASFFreeGamesOptions Options => OptionsField;
private ASFFreeGamesOptions OptionsField = new();
- private readonly ICollectIntervalManager CollectIntervalManager;
+ private readonly CollectIntervalManager CollectIntervalManager;
public ASFFreeGamesPlugin() {
CommandDispatcher = new CommandDispatcher(Options);
@@ -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 60%
rename from ASFFreeGames/Commands/GetIPCommand.cs
rename to ASFFreeGames/Commands/GetIp/GetIPCommand.cs
index 8419944..62f98e2 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,16 @@ internal sealed class GetIPCommand : IBotCommand {
}
try {
- ObjectResponse? result = await web.UrlGetToJsonObject(new Uri(GetIPAddressUrl)).ConfigureAwait(false);
- string origin = result?.Content?.Value("origin") ?? "";
+#pragma warning disable CAC001
+#pragma warning disable CA2007
+ await using StreamResponse? result = await web.UrlGetToStream(new Uri(GetIPAddressUrl), cancellationToken: cancellationToken).ConfigureAwait(false);
+#pragma warning restore CA2007
+#pragma warning restore CAC001
+
+ 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..bb8a636 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,8 @@ 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);
+ fs.SetLength(fs.Position);
}
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 e9ef9ae..ac9f713 100644
--- a/ASFFreeGames/RandomUtils.cs
+++ b/ASFFreeGames/RandomUtils.cs
@@ -1,77 +1,83 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
+using System.Threading;
namespace Maxisoft.ASF;
#nullable enable
public static class RandomUtils {
- ///
- /// Generates a random number from a normal distribution with the specified mean and standard deviation.
- ///
- /// The random number generator to use.
- /// The mean of the normal distribution.
- /// The standard deviation of the normal distribution.
- /// A random number from the normal distribution.
- ///
- /// This method uses the Box-Muller transform to convert two uniformly distributed random numbers into two normally distributed random numbers.
- ///
- public static double NextGaussian([NotNull] this RandomNumberGenerator random, double mean, double standardDeviation) {
- Debug.Assert(random != null, nameof(random) + " != null");
-
- // Generate two uniform random numbers
- Span bytes = stackalloc byte[8];
- random.GetBytes(bytes);
- double u1 = BitConverter.ToUInt32(bytes) / (double) uint.MaxValue;
- random.GetBytes(bytes);
- double u2 = BitConverter.ToUInt32(bytes) / (double) uint.MaxValue;
-
- // Apply the Box-Muller formula
- double randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
-
- // Scale and shift to get a random number with the desired mean and standard deviation
- double randNormal = mean + (standardDeviation * randStdNormal);
-
- return randNormal;
- }
+ internal sealed class GaussianRandom {
- internal sealed class GaussianRandom : RandomNumberGenerator {
// A flag to indicate if there is a stored value for the next Gaussian number
- private bool HasNextGaussian;
+ private int HasNextGaussian;
+
+ private const int True = 1;
+ private const int False = 0;
// 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)];
- public override void GetNonZeroBytes(byte[] data) => Fill(data);
+ 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);
+ }
- private double NextDouble() {
- if (HasNextGaussian) {
- HasNextGaussian = false;
+ fill(bytes);
+ int c = 0;
+
+ for (int i = 0; i < data.Length; i++) {
+ byte value;
+ do {
+ value = bytes[c];
+ c++;
+
+ if (c >= bytes.Length) {
+ fill(bytes);
+ c = 0;
+ }
+ } while (value == 0);
+
+ data[i] = value;
+ }
+ }
+
+ private double NextDouble() {
+ if (Interlocked.CompareExchange(ref HasNextGaussian, False, True) == True) {
return NextGaussianValue;
}
- // Generate two uniform random numbers
- Span bytes = stackalloc byte[8];
- GetBytes(bytes);
- float u1 = BitConverter.ToUInt32(bytes) / (float) uint.MaxValue;
- GetBytes(bytes);
- float u2 = BitConverter.ToUInt32(bytes) / (float) uint.MaxValue;
+ Span bytes = stackalloc byte[2 * sizeof(long)];
+
+ Span ulongs = MemoryMarshal.Cast(bytes);
+ double u1;
+
+ do {
+ GetNonZeroBytes(bytes);
+ u1 = ulongs[0] / (double) ulong.MaxValue;
+ } while (u1 <= double.Epsilon);
- // Apply the Box-Muller formula
- float r = MathF.Sqrt(-2.0f * MathF.Log(u1));
- float theta = 2.0f * MathF.PI * u2;
+ double u2 = ulongs[1] / (double) ulong.MaxValue;
- // Store one of the values for next time
- NextGaussianValue = r * MathF.Sin(theta);
- HasNextGaussian = true;
+ // Box-Muller formula
+ double r = Math.Sqrt(-2.0 * Math.Log(u1));
+ double theta = 2.0 * Math.PI * u2;
- // Return the other value
- return r * MathF.Cos(theta);
+ if (Interlocked.CompareExchange(ref HasNextGaussian, True, False) == False) {
+ NextGaussianValue = r * Math.Sin(theta);
+ }
+
+ return r * Math.Cos(theta);
}
///
@@ -83,9 +89,15 @@ private double NextDouble() {
///
/// This method uses the overridden NextDouble method to get a normally distributed random number.
///
- public double NextGaussian(double mean, double standardDeviation) =>
+ public double NextGaussian(double mean, double standardDeviation) {
+ // Use the overridden NextDouble method to get a normally distributed random
+ double rnd;
- // Use the overridden NextDouble method to get a normally distributed random number
- mean + (standardDeviation * NextDouble());
+ do {
+ rnd = NextDouble();
+ } while (!double.IsFinite(rnd));
+
+ return mean + (standardDeviation * rnd);
+ }
}
}
diff --git a/ASFFreeGames/Reddit/RedditHelper.cs b/ASFFreeGames/Reddit/RedditHelper.cs
index 323a06d..be5c0b9 100644
--- a/ASFFreeGames/Reddit/RedditHelper.cs
+++ b/ASFFreeGames/Reddit/RedditHelper.cs
@@ -2,31 +2,35 @@
using System.Buffers;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+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 +38,26 @@ public static async ValueTask> GetGames() {
return result;
}
- ObjectResponse? jsonPayload = null;
+ JsonNode jsonPayload = await GetPayload(webBrowser, cancellationToken).ConfigureAwait(false) ?? JsonNode.Parse("{}")!;
try {
- jsonPayload = await TryGetPayload(webBrowser).ConfigureAwait(false);
- }
- catch (Exception exception) when (exception is JsonException or IOException) {
- return result;
+ 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");
- if (jsonPayload is null) {
return result;
}
- // 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);
- }
- }
+ JsonNode? childrenElement = jsonPayload["data"]?["children"];
- return result; // Return early if children is not found or not an array
+ 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 +65,39 @@ 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;
+
+ try {
+ date = checked((long) (commentData["created_utc"]?.GetValue() ?? 0));
+ }
+ catch (Exception e) when (e is FormatException or InvalidOperationException) {
+ date = 0;
+ }
+
+ if (!double.IsNormal(date) || (date <= 0)) {
+ 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 +118,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 +147,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 +166,58 @@ 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) {
- try {
- return await webBrowser.UrlGetToJsonObject(GetUrl(), rateLimitingDelay: 500).ConfigureAwait(false);
- }
+ private static async ValueTask GetPayload(WebBrowser webBrowser, CancellationToken cancellationToken, uint retry = 5) {
+ StreamResponse? stream = null;
+
+ for (int t = 0; t < retry; t++) {
+ try {
+ 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 (stream.StatusCode.IsServerErrorCode()) {
+ throw new RedditServerException($"Reddit server error: {stream.StatusCode}", stream.StatusCode);
+ }
- 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 (Exception e) when (e is JsonException or IOException or RedditServerException or HttpRequestException) {
+ // If no RedditServerException was thrown, re-throw the original Exception
+ if (t + 1 == retry) {
+ throw;
+ }
+ }
+ finally {
+ if (stream is not null) {
+ await stream.DisposeAsync().ConfigureAwait(false);
+ }
+
+ stream = null;
}
- // If no RedditServerException was thrown, re-throw the original JsonReaderException
- throw;
+ await Task.Delay((2 << t) * 100, cancellationToken).ConfigureAwait(false);
+ cancellationToken.ThrowIfCancellationRequested();
}
+
+ return JsonNode.Parse("{}");
+ }
+
+ ///
+ /// 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 25420d7..6bf6713 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,8 +3,8 @@
ASFFreeGames
- 1.4.1.0
- net7.0
+ 1.5.1.0
+ net8.0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a4d766d..c120255 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,9 +1,10 @@
-
+
+