Skip to content

Commit

Permalink
Merge branch '1.5'
Browse files Browse the repository at this point in the history
  • Loading branch information
Citrinate committed Jun 2, 2024
2 parents 12c678e + e1d3048 commit 4eb61d7
Show file tree
Hide file tree
Showing 15 changed files with 222 additions and 37 deletions.
17 changes: 17 additions & 0 deletions FreePackages.Tests/Filters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,21 @@ public void CanFilterByWishlist() {

Assert.IsTrue(PackageFilter.IsAppWantedByFilter(app, Filter));
}

[TestMethod]
public void CanFilterByReleaseDate() {
var app = new FilterableApp(KeyValue.LoadAsText("app_which_is_free.txt"));

Filter.MinDaysOld = 0;

Assert.IsTrue(PackageFilter.IsAppWantedByFilter(app, Filter));

Filter.MinDaysOld = 1;

Assert.IsFalse(PackageFilter.IsAppWantedByFilter(app, Filter));

Filter.MinDaysOld = 20000;

Assert.IsTrue(PackageFilter.IsAppWantedByFilter(app, Filter));
}
}
14 changes: 11 additions & 3 deletions FreePackages/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;

namespace FreePackages {
internal static class Commands {
Expand Down Expand Up @@ -69,7 +71,7 @@ internal static class Commands {
}

if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, "Free Packages plugin not enabled");
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}

return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ClearQueue());
Expand Down Expand Up @@ -103,7 +105,7 @@ internal static class Commands {
}

if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, "Free Packages plugin not enabled");
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}

return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].GetStatus());
Expand All @@ -127,7 +129,7 @@ internal static class Commands {
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}

private static string? ResponseQueueLicense(Bot bot, EAccess access, string licenses, bool useFilter = false) {
private static string? ResponseQueueLicense(Bot bot, EAccess access, string licenses, bool useFilter = false, [CallerMemberName] string? previousMethodName = null) {
if (access < EAccess.Master) {
return null;
}
Expand Down Expand Up @@ -178,6 +180,10 @@ internal static class Commands {
response.AppendLine(FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].AddPackage(packageType, gameID, useFilter)));
}

if (previousMethodName == nameof(Response)) {
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
}

return response.Length > 0 ? response.ToString() : null;
}

Expand All @@ -196,6 +202,8 @@ internal static class Commands {

List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));

return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}

Expand Down
1 change: 1 addition & 0 deletions FreePackages/Data/ASFInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ private static async Task DoUpdate() {

PackageHandler.Handlers.Values.ToList().ForEach(x => x.BotCache.AddChanges(appIDs, packageIDs));
FreePackages.GlobalCache.UpdateASFInfoItemCount(itemCount);
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
}
}
}
4 changes: 3 additions & 1 deletion FreePackages/Data/BotCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ internal BotCache(string filePath) : this() {
return null;
}

botCache.Packages = new(botCache.Packages.GroupBy(package => package, new PackageComparer()).Select(group => group.First()), new PackageComparer());
botCache.FilePath = filePath;

return botCache;
Expand Down Expand Up @@ -121,13 +122,14 @@ internal bool RemovePackage(Package package) {
}

internal bool RemoveAppPackages(HashSet<uint> appIDsToRemove) {
Packages.Where(x => appIDsToRemove.Contains(x.ID)).ToList().ForEach(x => Packages.Remove(x));
Packages.Where(x => x.Type == EPackageType.App && appIDsToRemove.Contains(x.ID)).ToList().ForEach(x => Packages.Remove(x));
Utilities.InBackground(Save);

return true;
}

internal Package? GetNextPackage() {
// Return the package which should be activated first, prioritizing first packages which have a start and end date
ulong now = DateUtils.DateTimeToUnixTime(DateTime.UtcNow);
Package? package = Packages.FirstOrDefault(x => x.StartTime != null && now > x.StartTime);
if (package != null) {
Expand Down
3 changes: 3 additions & 0 deletions FreePackages/Data/FilterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ internal sealed class FilterConfig {
[JsonInclude]
internal bool WishlistOnly { get; set; } = false;

[JsonInclude]
internal uint MinDaysOld { get; set; } = 0;

[JsonConstructor]
internal FilterConfig() { }
}
Expand Down
2 changes: 2 additions & 0 deletions FreePackages/Data/FilterableApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal sealed class FilterableApp {
internal uint PlayTestType;
internal List<string>? OSList;
internal uint DeckCompatibility;
internal DateTime SteamReleaseDate;
internal bool Hidden;

internal FilterableApp(SteamApps.PICSProductInfoCallback.PICSProductInfo productInfo) : this(productInfo.ID, productInfo.KeyValues) {}
Expand Down Expand Up @@ -55,6 +56,7 @@ internal FilterableApp(uint id, KeyValue kv) {
PlayTestType = kv["extended"]["playtest_type"].AsUnsignedInteger();
OSList = kv["common"]["oslist"].AsString()?.ToUpper().Split(",").ToList();
DeckCompatibility = kv["common"]["steam_deck_compatibility"]["category"].AsUnsignedInteger();
SteamReleaseDate = DateTimeOffset.FromUnixTimeSeconds(kv["common"]["steam_release_date"].AsUnsignedInteger()).UtcDateTime;
Hidden = kv["common"] == KeyValue.Invalid;

// Fix the category for games which do have trading cards, but which don't have the trading card category, Ex: https://steamdb.info/app/316260/
Expand Down
10 changes: 9 additions & 1 deletion FreePackages/Data/FilterablePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace FreePackages {
internal sealed class FilterablePackage {
internal bool IsNew;
internal bool IsNew; // This is used when finding DLC for new games added to account, and is not related to any Steam package property
internal List<FilterableApp> PackageContents = new();
internal HashSet<uint> PackageContentIDs;
internal HashSet<uint> PackageContentParentIDs = new();
Expand All @@ -18,6 +18,7 @@ internal sealed class FilterablePackage {
internal ulong ExpiryTime;
internal ulong StartTime;
internal uint DontGrantIfAppIDOwned;
internal uint MustOwnAppToPurchase;
internal List<string>? RestrictedCountries;
internal bool OnlyAllowRestrictedCountries;
internal List<string>? PurchaseRestrictedCountries;
Expand All @@ -38,6 +39,7 @@ internal FilterablePackage(uint id, KeyValue kv, bool isNew) {
ExpiryTime = kv["extended"]["expirytime"].AsUnsignedLong();
StartTime = kv["extended"]["starttime"].AsUnsignedLong();
DontGrantIfAppIDOwned = kv["extended"]["dontgrantifappidowned"].AsUnsignedInteger();
MustOwnAppToPurchase = kv["extended"]["mustownapptopurchase"].AsUnsignedInteger();
RestrictedCountries = kv["extended"]["restrictedcountries"].AsString()?.ToUpper().Split(" ").ToList();
OnlyAllowRestrictedCountries = kv["extended"]["onlyallowrestrictedcountries"].AsBoolean();
PurchaseRestrictedCountries = kv["extended"]["purchaserestrictedcountries"].AsString()?.ToUpper().Split(" ").ToList();
Expand Down Expand Up @@ -111,6 +113,12 @@ internal bool IsAvailable() {
return false;
}

if (ID == 17906) {
// Special case: Anonymous Dedicated Server Comp (https://steamdb.info/sub/17906/)
// This always returns AccessDenied/InvalidPackage
return false;
}

return true;
}

Expand Down
37 changes: 37 additions & 0 deletions FreePackages/Data/SharedExternalResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Threading;
using System.Threading.Tasks;

// This resource may be used zero or more times independently and, when used, needs to be fetched from an external source.
// If it's used zero times we don't fetch it at all.
// If it's used once or more then we only fetch it once.

namespace FreePackages {
internal sealed class SharedExternalResource<T> {
private SemaphoreSlim FetchSemaphore = new SemaphoreSlim(1, 1);
private T? Resource;
private bool Fetched = false;

internal SharedExternalResource() {}

internal async Task<T?> Fetch(Func<Task<T?>> fetchResource) {
if (Fetched) {
return Resource;
}

await FetchSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (Fetched) {
return Resource;
}

Resource = await fetchResource().ConfigureAwait(false);
Fetched = true;

return Resource;
} finally {
FetchSemaphore.Release();
}
}
}
}
2 changes: 1 addition & 1 deletion FreePackages/FreePackages.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Authors>Citrinate</Authors>
<AssemblyVersion>1.4.5.2</AssemblyVersion>
<AssemblyVersion>1.5.0.0</AssemblyVersion>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TargetFramework>net8.0</TargetFramework>
Expand Down
63 changes: 62 additions & 1 deletion FreePackages/Handlers/PackageFilter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AngleSharp.Dom;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam.Integration;
using ArchiSteamFarm.Web.Responses;
using SteamKit2;

namespace FreePackages {
Expand Down Expand Up @@ -129,6 +132,11 @@ internal bool IsAppWantedByFilter(FilterableApp app, FilterConfig filter) {
// Unwated to due not being wishlisted or followed on the Steam storefront
return false;
}

if (filter.MinDaysOld > 0 && DateTime.UtcNow.AddDays(-filter.MinDaysOld) > app.SteamReleaseDate) {
// Unwanted because the app isn't new enough
return false;
}

return true;
}
Expand Down Expand Up @@ -183,6 +191,28 @@ internal bool IsAppIgnoredByFilter(FilterableApp app, FilterConfig filter) {
return false;
}

internal bool IsAppFreeAndValidOnStore(AppDetails? appDetails) {
if (appDetails == null) {
// Indeterminate, assume the app is free and valid
return true;
}

if (!appDetails.Success) {
// App doesn't have a store page
// Usually this is true, but not always. Example: https://store.steampowered.com/api/appdetails/?appids=317780 (on May 13, 2024)
// App 317780, also passes all of the checks below, but cannot be activated and doesn't have a store page. It's type is listed as "advertising".
return false;
}

bool isFree = appDetails?.Data?.IsFree ?? false;
if (!isFree) {
// App is not free
return false;
}

return true;
}

internal bool IsRedeemablePackage(FilterablePackage package) {
if (UserData == null) {
throw new InvalidOperationException(nameof(UserData));
Expand All @@ -207,6 +237,11 @@ internal bool IsRedeemablePackage(FilterablePackage package) {
}

if (package.DontGrantIfAppIDOwned > 0 && OwnedAppIDs.Contains(package.DontGrantIfAppIDOwned)) {
// Owns an app that blocks activation
return false;
}

if (package.MustOwnAppToPurchase > 0 && !OwnedAppIDs.Contains(package.MustOwnAppToPurchase)) {
// Don't own required app
return false;
}
Expand Down Expand Up @@ -264,7 +299,7 @@ internal bool IsPackageIgnoredByFilter(FilterablePackage package, FilterConfig f
internal bool IsRedeemablePlaytest(FilterableApp app) {
// More than half of playtests we try to join will be invalid.
// Some of these will be becase there's no free packages (which we can't determine here), Ex: playtest is activated by key: https://steamdb.info/sub/858277/
// For most, There seems to be no difference at all between invalid playtest and valid ones. The only way to resolve these would be to scrape the parent's store page.
// For most, There seems to be no difference at all between invalid playtest and valid ones. The only way to resolve these is to scrape the parent's store page.

if (app.Parent == null) {
return false;
Expand Down Expand Up @@ -318,6 +353,32 @@ internal bool IsPlaytestWantedByFilter(FilterableApp app, FilterConfig filter) {
return true;
}

internal bool IsPlaytestValidOnStore(HtmlDocumentResponse? storePage) {
if (storePage == null) {
// Indeterminate, assume the playtest is valid
return true;
}

bool hasStorePage = storePage.FinalUri != ArchiWebHandler.SteamStoreURL;
if (!hasStorePage) {
// App doesnt have a store page (redirects to homepage)
return false;
}

if (storePage.Content == null || !storePage.StatusCode.IsSuccessCode()) {
// Indeterminate (this will catch age gated store pages), assume the playtest is valid
return true;
}

bool hasPlaytestButton = storePage.Content.SelectNodes("//script").Any(static node => node.TextContent.Contains("RequestPlaytestAccess"));
if (!hasPlaytestButton) {
// Playtest is not active (doesn't have a "Request Access" button on store page)
return false;
}

return true;
}

internal bool FilterOnlyAllowsPackages(FilterConfig filter) {
if (filter.NoCostOnly) {
// NoCost is a property value that only applies to packages, so ignore all non-packages
Expand Down
Loading

0 comments on commit 4eb61d7

Please sign in to comment.