From 9eadfe688a7e924285deaec6a2f177bac37088a3 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Fri, 22 May 2026 23:31:10 +0300 Subject: [PATCH 01/14] Initial GameEngineChecker .NET rewrite --- source/Generic/GameEngineChecker/App.xaml | 12 + .../GameEngineChecker/GameEngineChecker.cs | 195 +++++++++++++++++ .../GameEngineChecker.csproj | 105 +++++++++ .../GameEngineChecker/GameEngineChecker.psm1 | 205 ------------------ .../GameEngineChecker/GameEngineChecker.sln | 31 +++ .../GameEngineCheckerSettings.cs | 91 ++++++++ .../GameEngineCheckerSettingsView.xaml | 14 ++ .../GameEngineCheckerSettingsView.xaml.cs | 25 +++ .../Interfaces/IEnginesParser.cs | 9 + .../Interfaces/IGamesFilter.cs | 9 + .../Interfaces/IPcGamingWikiClient.cs | 11 + .../Interfaces/IPcGamingWikiLinkProvider.cs | 12 + .../Interfaces/IRateLimiter.cs | 10 + .../GameEngineChecker/Interfaces/ITagger.cs | 11 + .../Models/PcGamingWiki/CargoQueryItem.cs | 7 + .../Models/PcGamingWiki/Infobox.cs | 8 + .../PcGamingWikiEngineResponse.cs | 9 + .../Properties/AssemblyInfo.cs | 36 +++ .../Services/EnginesParser.cs | 18 ++ .../Services/GameEngineCheckerService.cs | 76 +++++++ .../GameEngineChecker/Services/GamesFilter.cs | 24 ++ .../Services/PcGamingWikiClient.cs | 66 ++++++ .../Services/PcGamingWikiLinkProvider.cs | 51 +++++ .../GameEngineChecker/Services/RateLimiter.cs | 64 ++++++ .../GameEngineChecker/Services/Tagger.cs | 44 ++++ .../Generic/GameEngineChecker/extension.yaml | 8 +- .../Generic/GameEngineChecker/packages.config | 5 + .../GameEngineChecker.Tests.csproj | 118 ++++++++++ .../Properties/AssemblyInfo.cs | 20 ++ .../Services/EnginesParserTests.cs | 63 ++++++ .../Services/GameEngineCheckerServiceTests.cs | 92 ++++++++ .../Services/GamesFilterTests.cs | 63 ++++++ .../Services/PcGamingWikiLinkProviderTests.cs | 84 +++++++ .../Services/RateLimiterTests.cs | 63 ++++++ .../Services/TaggerTests.cs | 83 +++++++ .../TestableItemCollection.cs | 190 ++++++++++++++++ .../GameEngineChecker.Tests/app.config | 11 + .../GameEngineChecker.Tests/packages.config | 17 ++ source/PlayniteExtensions.sln | 7 + 39 files changed, 1758 insertions(+), 209 deletions(-) create mode 100644 source/Generic/GameEngineChecker/App.xaml create mode 100644 source/Generic/GameEngineChecker/GameEngineChecker.cs create mode 100644 source/Generic/GameEngineChecker/GameEngineChecker.csproj delete mode 100644 source/Generic/GameEngineChecker/GameEngineChecker.psm1 create mode 100644 source/Generic/GameEngineChecker/GameEngineChecker.sln create mode 100644 source/Generic/GameEngineChecker/GameEngineCheckerSettings.cs create mode 100644 source/Generic/GameEngineChecker/GameEngineCheckerSettingsView.xaml create mode 100644 source/Generic/GameEngineChecker/GameEngineCheckerSettingsView.xaml.cs create mode 100644 source/Generic/GameEngineChecker/Interfaces/IEnginesParser.cs create mode 100644 source/Generic/GameEngineChecker/Interfaces/IGamesFilter.cs create mode 100644 source/Generic/GameEngineChecker/Interfaces/IPcGamingWikiClient.cs create mode 100644 source/Generic/GameEngineChecker/Interfaces/IPcGamingWikiLinkProvider.cs create mode 100644 source/Generic/GameEngineChecker/Interfaces/IRateLimiter.cs create mode 100644 source/Generic/GameEngineChecker/Interfaces/ITagger.cs create mode 100644 source/Generic/GameEngineChecker/Models/PcGamingWiki/CargoQueryItem.cs create mode 100644 source/Generic/GameEngineChecker/Models/PcGamingWiki/Infobox.cs create mode 100644 source/Generic/GameEngineChecker/Models/PcGamingWiki/PcGamingWikiEngineResponse.cs create mode 100644 source/Generic/GameEngineChecker/Properties/AssemblyInfo.cs create mode 100644 source/Generic/GameEngineChecker/Services/EnginesParser.cs create mode 100644 source/Generic/GameEngineChecker/Services/GameEngineCheckerService.cs create mode 100644 source/Generic/GameEngineChecker/Services/GamesFilter.cs create mode 100644 source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs create mode 100644 source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs create mode 100644 source/Generic/GameEngineChecker/Services/RateLimiter.cs create mode 100644 source/Generic/GameEngineChecker/Services/Tagger.cs create mode 100644 source/Generic/GameEngineChecker/packages.config create mode 100644 source/GenericTests/GameEngineChecker.Tests/GameEngineChecker.Tests.csproj create mode 100644 source/GenericTests/GameEngineChecker.Tests/Properties/AssemblyInfo.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/Services/EnginesParserTests.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/Services/GameEngineCheckerServiceTests.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/Services/GamesFilterTests.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/Services/RateLimiterTests.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/Services/TaggerTests.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/TestableItemCollection.cs create mode 100644 source/GenericTests/GameEngineChecker.Tests/app.config create mode 100644 source/GenericTests/GameEngineChecker.Tests/packages.config diff --git a/source/Generic/GameEngineChecker/App.xaml b/source/Generic/GameEngineChecker/App.xaml new file mode 100644 index 0000000000..0e3490bb12 --- /dev/null +++ b/source/Generic/GameEngineChecker/App.xaml @@ -0,0 +1,12 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/Generic/GameEngineChecker/Views/ProgressView.xaml.cs b/source/Generic/GameEngineChecker/Views/ProgressView.xaml.cs new file mode 100644 index 0000000000..89928d8297 --- /dev/null +++ b/source/Generic/GameEngineChecker/Views/ProgressView.xaml.cs @@ -0,0 +1,17 @@ +using GameEngineChecker.ViewModels; +using System.Windows.Controls; + +namespace GameEngineChecker.Views +{ + /// + /// Interaction logic for ProgressView.xaml + /// + public partial class ProgressView : UserControl + { + public ProgressView(ProgressViewModel progressViewModel) + { + InitializeComponent(); + DataContext = progressViewModel; + } + } +} \ No newline at end of file diff --git a/source/GenericTests/GameEngineChecker.Tests/Services/GameEngineCheckerServiceTests.cs b/source/GenericTests/GameEngineChecker.Tests/Services/GameEngineCheckerServiceTests.cs index e46f829d0c..5dd3571d66 100644 --- a/source/GenericTests/GameEngineChecker.Tests/Services/GameEngineCheckerServiceTests.cs +++ b/source/GenericTests/GameEngineChecker.Tests/Services/GameEngineCheckerServiceTests.cs @@ -46,7 +46,7 @@ public async Task AddGameEngineTags_AddsEngineTags_WhenTagsShouldBeAdded() SetupSuccessfulRun(game, link, engines, parsedEngines); // Act - await _sut.AddGameEngineTags(new List { game }, CancellationToken.None); + await _sut.AddGameEngineTags(new List { game }, x => { }, CancellationToken.None); // Assert A.CallTo(() => _tagger.AddEngineTags(game, parsedEngines, CancellationToken.None)).MustHaveHappenedOnceExactly(); @@ -58,9 +58,9 @@ public async Task AddGameEngineTags_DoesNotGenerateLink_WhenTagsShouldNotBeAdded // Arrange var game = _fixture.Create(); A.CallTo(() => _gamesFilter.ShouldTheGameBeProcessed(game)).Returns(false); - + // Act - await _sut.AddGameEngineTags(new List { game }, CancellationToken.None); + await _sut.AddGameEngineTags(new List { game }, x => { }, CancellationToken.None); // Assert A.CallTo(() => _pcGamingWikiLinkProvider.GetLink(A._, CancellationToken.None)).MustNotHaveHappened(); @@ -75,7 +75,7 @@ public async Task AddGameEngineTags_DoesNotCallPcGamingWiki_WhenLinkCouldNotBeGe A.CallTo(() => _pcGamingWikiLinkProvider.GetLink(game, A._)).Returns(null); // Act - await _sut.AddGameEngineTags(new List { game }, CancellationToken.None); + await _sut.AddGameEngineTags(new List { game }, x => { }, CancellationToken.None); // Assert A.CallTo(() => _pcGamingWikiClient.GetEngines(A._, A._, A._)).MustNotHaveHappened(); @@ -89,4 +89,4 @@ private void SetupSuccessfulRun(Game game, Uri link, string engines, List _enginesParser.Parse(engines)).Returns(parsedEngines); } } -} +} \ No newline at end of file diff --git a/source/GenericTests/GameEngineChecker.Tests/Services/GamesFilterTests.cs b/source/GenericTests/GameEngineChecker.Tests/Services/GamesFilterTests.cs index 911b43a83e..88c227a5c5 100644 --- a/source/GenericTests/GameEngineChecker.Tests/Services/GamesFilterTests.cs +++ b/source/GenericTests/GameEngineChecker.Tests/Services/GamesFilterTests.cs @@ -46,6 +46,20 @@ public void ShouldTheGameBeProcessed_ReturnsTrue_WhenGameHasNoEngineTags() Assert.True(result); } + [Fact] + public void ShouldTheGameBeProcessed_ReturnsTrue_WhenGameHasNoTags() + { + // Arrange + var game = _fixture.Create(); + game.TagIds = null; + + // Act + var result = _sut.ShouldTheGameBeProcessed(game); + + // Assert + Assert.True(result); + } + [Fact] public void ShouldTheGameBeProcessed_ReturnsFalse_WhenGameHasAnyEngineTag() { diff --git a/source/GenericTests/GameEngineChecker.Tests/Services/TaggerTests.cs b/source/GenericTests/GameEngineChecker.Tests/Services/TaggerTests.cs index 10be143da4..71370e0ebd 100644 --- a/source/GenericTests/GameEngineChecker.Tests/Services/TaggerTests.cs +++ b/source/GenericTests/GameEngineChecker.Tests/Services/TaggerTests.cs @@ -48,6 +48,23 @@ public void AddEngineTags_AddsNewTagAndUpdatesTheGame_WhenTagDoesNotExist() Assert.Contains(tag.Id, game.TagIds); } + [Fact] + public void AddEngineTags_AddsNewTagAndUpdatesTheGame_WhenGameHasNoTags() + { + // Arrange + var game = _fixture.Create(); + var engines = new List { "Unity" }; + game.TagIds = null; + + // Act + _sut.AddEngineTags(game, engines, CancellationToken.None); + + // Assert + var tag = Assert.Single(_api.Database.Tags); + Assert.Equal("[Engine] Unity", tag.Name); + Assert.Contains(tag.Id, game.TagIds); + } + [Fact] public void AddEngineTags_DoesNotAddTheTagToTheGame_WhenGameHasTheTag() { From be0767852605903e2ab4fa17b0b06af56e14ada3 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sat, 23 May 2026 23:29:01 +0300 Subject: [PATCH 05/14] Fix couple more bugs with null lists on game --- .../Generic/GameEngineChecker/GameEngineChecker.cs | 4 ++++ .../GameEngineChecker/Localization/en_US.xaml | 2 ++ .../Services/PcGamingWikiLinkProvider.cs | 7 ++++++- .../Services/PcGamingWikiLinkProviderTests.cs | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/source/Generic/GameEngineChecker/GameEngineChecker.cs b/source/Generic/GameEngineChecker/GameEngineChecker.cs index 4b4902cfc5..2064be87d1 100644 --- a/source/Generic/GameEngineChecker/GameEngineChecker.cs +++ b/source/Generic/GameEngineChecker/GameEngineChecker.cs @@ -96,6 +96,10 @@ void ReportProgressAction(float progress) catch (Exception ex) { Logger.Error(ex, "Failure while adding engines to games."); + PlayniteApi.Notifications.Add( + "game_engine_checker__add_error", + string.Format(ResourceProvider.GetString("LOCGame_Engine_Checker_ResultsErrorMessage"), ex.Message, ex.StackTrace), + NotificationType.Error); } } diff --git a/source/Generic/GameEngineChecker/Localization/en_US.xaml b/source/Generic/GameEngineChecker/Localization/en_US.xaml index 209f9c82bd..9dd23f1f02 100644 --- a/source/Generic/GameEngineChecker/Localization/en_US.xaml +++ b/source/Generic/GameEngineChecker/Localization/en_US.xaml @@ -6,5 +6,7 @@ Couldn't download game information of "{0}" from PCGW. Error: {1} Finished. Added engine tag to {0} game(s). + Failure while adding engines to games: {0} +{1} Error downloading Steam AppList database. Error: {0} \ No newline at end of file diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs index 4a74edecca..734919d9f8 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs @@ -20,11 +20,16 @@ public Task GetLink(Game game, CancellationToken cancellationToken) } if (game.PluginId == Guid.Parse("AEBE8B7C-6DC3-4A66-AF31-E7375C6B5E9E") // GOG - || game.PluginId == Guid.Parse("03689811-3F33-4DFB-A121-2EE168FB9A5C")) // GOG OSS + || game.PluginId == Guid.Parse("03689811-3F33-4DFB-A121-2EE168FB9A5C")) // GOG OSS { return Task.FromResult(GetGogGameLink(game.GameId)); } + if (game.Links == null) + { + return Task.FromResult(null); + } + foreach (var link in game.Links) { var match = _steamLinkRegex.Match(link.Url); diff --git a/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs b/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs index 621e814bab..549cd2408f 100644 --- a/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs +++ b/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs @@ -68,6 +68,20 @@ public async Task GetLink_UseSteamLink_WhenGameIsNonSteamNonGogGameAndHasSteamLi Assert.Equal(new Uri($@"https://www.pcgamingwiki.com/w/api.php?action=cargoquery&format=json&tables=Infobox_game&fields=Engines,_pageName=title&where=Steam_AppID HOLDS ""3634520"""), result); } + [Fact] + public async Task GetLink_ReturnNull_WhenGameIsNonSteamNonGogAndHasNoLinks() + { + // Arrange + var game = _fixture.Create(); + game.Links = null; + + // Act + var result = await _sut.GetLink(game, CancellationToken.None); + + // Assert + Assert.Null(result); + } + [Fact] public async Task GetLink_ReturnNull_WhenGameLinkNotGenerated() { From 1c7a80979aef5649e7a5bd936b2dce462b289d6d Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 11:05:14 +0300 Subject: [PATCH 06/14] Add all engines, when multiple entries found --- .../GameEngineChecker/GameEngineChecker.cs | 35 +++++++++---------- .../Services/PcGamingWikiClient.cs | 12 +++++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/source/Generic/GameEngineChecker/GameEngineChecker.cs b/source/Generic/GameEngineChecker/GameEngineChecker.cs index 2064be87d1..a78f90491f 100644 --- a/source/Generic/GameEngineChecker/GameEngineChecker.cs +++ b/source/Generic/GameEngineChecker/GameEngineChecker.cs @@ -79,7 +79,7 @@ private async Task AddTagsToGames(IReadOnlyList games) _tagger); using (var cancellationTokenSource = new CancellationTokenSource()) - using (var progressViewModel = ShowProgressDialog(cancellationTokenSource, true)) + using (var progressViewModel = ShowProgressDialog(cancellationTokenSource)) { void ReportProgressAction(float progress) { @@ -87,6 +87,8 @@ void ReportProgressAction(float progress) } var addedCount = await gameEngineCheckerService.AddGameEngineTags(games, ReportProgressAction, cancellationTokenSource.Token); + + Logger.Info($"Successfully added game engine to {addedCount} out of {games.Count} games."); PlayniteApi.Notifications.Add( "game_engine_checker__added_count", string.Format(ResourceProvider.GetString("LOCGame_Engine_Checker_ResultsMessage"), addedCount), @@ -103,25 +105,22 @@ void ReportProgressAction(float progress) } } - private ProgressViewModel ShowProgressDialog(CancellationTokenSource cts, bool showProgressBar) + private ProgressViewModel ShowProgressDialog(CancellationTokenSource cts) { var progressViewModel = new ProgressViewModel(PlayniteApi, cts); - if (showProgressBar) - { - PlayniteApi.MainView.UIDispatcher.Invoke(() => - { - var window = ShowDialog( - new ProgressView(progressViewModel), - 100, - 250, - ResourceProvider.GetString("LOCGame_Engine_Checker_ProgressTitle"), - false, - false); - - progressViewModel.SetWindow(window); - } - ); - } + PlayniteApi.MainView.UIDispatcher.Invoke(() => + { + var window = ShowDialog( + new ProgressView(progressViewModel), + 100, + 250, + ResourceProvider.GetString("LOCGame_Engine_Checker_ProgressTitle"), + false, + false); + + progressViewModel.SetWindow(window); + } + ); return progressViewModel; } diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs index f6a35e04df..b5f826f166 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs @@ -4,6 +4,7 @@ using Playnite.SDK; using Playnite.SDK.Models; using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; @@ -36,8 +37,15 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc response.EnsureSuccessStatusCode(); var parsedResponse = ParseResponse(responseString); - var engines = parsedResponse?.CargoQuery?.FirstOrDefault()?.Title?.Engines; - if (engines == null) + + var allFoundEntriesEngines = parsedResponse? + .CargoQuery? + .Where(x => x.Title?.Engines != null) + .Select(x => x.Title?.Engines) + .ToList() + ?? new List(); + var engines = string.Join(",", allFoundEntriesEngines); + if (string.IsNullOrEmpty(engines)) { _logger.Warn($"No engines found in response: {responseString}"); } From f4932467b5807f7c047b53f6b2e23a5f66dc30f5 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 11:26:09 +0300 Subject: [PATCH 07/14] Using wikipedia links, more logging --- .../Services/PcGamingWikiClient.cs | 2 +- .../Services/PcGamingWikiLinkProvider.cs | 26 ++++++++++++++----- .../GameEngineChecker/Services/Tagger.cs | 18 +++++++++++-- .../Services/PcGamingWikiLinkProviderTests.cs | 16 ++++++++++++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs index b5f826f166..3fd731db99 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs @@ -47,7 +47,7 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc var engines = string.Join(",", allFoundEntriesEngines); if (string.IsNullOrEmpty(engines)) { - _logger.Warn($"No engines found in response: {responseString}"); + _logger.Info($"No engines found in response: {responseString}"); } return engines; diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs index 734919d9f8..644b59ae77 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiLinkProvider.cs @@ -1,9 +1,9 @@ -using System; +using GameEngineChecker.Interfaces; +using Playnite.SDK.Models; +using System; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using GameEngineChecker.Interfaces; -using Playnite.SDK.Models; namespace GameEngineChecker.Services { @@ -11,6 +11,7 @@ public class PcGamingWikiLinkProvider : IPcGamingWikiLinkProvider { private const string UrlBase = "https://www.pcgamingwiki.com/w/api.php?action=cargoquery&format=json&tables=Infobox_game&fields=Engines,_pageName=title&where="; private readonly Regex _steamLinkRegex = new Regex(@"store\.steampowered\.com/app/(?\d+)", RegexOptions.Compiled); + private readonly Regex _wikipediaLinkRegex = new Regex(@"wikipedia\.org/wiki/(?[^/]+)", RegexOptions.Compiled); public Task GetLink(Game game, CancellationToken cancellationToken) { @@ -32,12 +33,19 @@ public Task GetLink(Game game, CancellationToken cancellationToken) foreach (var link in game.Links) { - var match = _steamLinkRegex.Match(link.Url); - if (match.Success) + var steamMatch = _steamLinkRegex.Match(link.Url); + if (steamMatch.Success) { - var gameId = match.Groups["appId"].Value; + var gameId = steamMatch.Groups["appId"].Value; return Task.FromResult(GetSteamGameLink(gameId)); } + + var wikipediaMatch = _wikipediaLinkRegex.Match(link.Url); + if (wikipediaMatch.Success) + { + var pageName = wikipediaMatch.Groups["pageName"].Value; + return Task.FromResult(GetWikipediaGameLink(pageName)); + } } return Task.FromResult(null); @@ -52,5 +60,11 @@ private static Uri GetGogGameLink(string gameId) { return new Uri($@"{UrlBase}GOGcom_ID HOLDS ""{gameId}"""); } + + private static Uri GetWikipediaGameLink(string pageName) + { + var pcWikiPageName = pageName.Replace('_', ' '); + return new Uri($@"{UrlBase}Wikipedia=""{pcWikiPageName}"""); + } } } \ No newline at end of file diff --git a/source/Generic/GameEngineChecker/Services/Tagger.cs b/source/Generic/GameEngineChecker/Services/Tagger.cs index c7ac561c68..eb9c058435 100644 --- a/source/Generic/GameEngineChecker/Services/Tagger.cs +++ b/source/Generic/GameEngineChecker/Services/Tagger.cs @@ -12,6 +12,7 @@ public class Tagger : ITagger { private const string TagPrefix = "[Engine]"; + private readonly ILogger _logger = LogManager.GetLogger(); private readonly IPlayniteAPI _api; private readonly SemaphoreSlim _semaphore; @@ -27,11 +28,17 @@ public void AddEngineTags(Game game, IReadOnlyCollection engines, Cancel { _semaphore.Wait(cancellationToken); var tagNames = engines.Select(x => $"{TagPrefix} {x}").ToList(); - var tags = _api.Database.Tags.Add(tagNames); + var tags = _api.Database.Tags.Add(tagNames).ToList(); var newTagsIdsForGame = tags .Select(x => x.Id) - .Except(game.TagIds ?? new List()); + .Except(game.TagIds ?? new List()) + .ToList(); + + if (newTagsIdsForGame.Count == 0) + { + return; + } if (game.TagIds == null) { @@ -40,6 +47,13 @@ public void AddEngineTags(Game game, IReadOnlyCollection engines, Cancel game.TagIds.AddRange(newTagsIdsForGame); _api.Database.Games.Update(game); + + var addedTagNames = newTagsIdsForGame + .Select(id => tags.FirstOrDefault(x => x.Id == id)) + .Where(x => x != null) + .Select(x => x.Name); + + _logger.Info($"Added game engine(s) {string.Join(", ", addedTagNames)} to {game.Name}"); } finally { diff --git a/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs b/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs index 549cd2408f..ad9f1a150e 100644 --- a/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs +++ b/source/GenericTests/GameEngineChecker.Tests/Services/PcGamingWikiLinkProviderTests.cs @@ -68,6 +68,22 @@ public async Task GetLink_UseSteamLink_WhenGameIsNonSteamNonGogGameAndHasSteamLi Assert.Equal(new Uri($@"https://www.pcgamingwiki.com/w/api.php?action=cargoquery&format=json&tables=Infobox_game&fields=Engines,_pageName=title&where=Steam_AppID HOLDS ""3634520"""), result); } + [Theory] + [InlineData("Wikipedia", "https://en.wikipedia.org/wiki/Need_for_Speed_III:_Hot_Pursuit")] + [InlineData("LINK!", "wikipedia.org/wiki/Need_for_Speed_III:_Hot_Pursuit")] + public async Task GetLink_UseWikipediaLink_WhenGameIsNonSteamNonGogGameAndHasWikipediaLink(string name, string url) + { + // Arrange + var game = _fixture.Create(); + game.Links.Add(new Link(name, url)); + + // Act + var result = await _sut.GetLink(game, CancellationToken.None); + + // Assert + Assert.Equal(new Uri($@"https://www.pcgamingwiki.com/w/api.php?action=cargoquery&format=json&tables=Infobox_game&fields=Engines,_pageName=title&where=Wikipedia=""Need for Speed III: Hot Pursuit"""), result); + } + [Fact] public async Task GetLink_ReturnNull_WhenGameIsNonSteamNonGogAndHasNoLinks() { From 494998b7ef3e66a4de6ae232387b7f4484955ccd Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 11:43:59 +0300 Subject: [PATCH 08/14] Reverted to first engine entry in results --- .../Services/PcGamingWikiClient.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs index 3fd731db99..08460240f4 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs @@ -37,17 +37,11 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc response.EnsureSuccessStatusCode(); var parsedResponse = ParseResponse(responseString); - - var allFoundEntriesEngines = parsedResponse? - .CargoQuery? - .Where(x => x.Title?.Engines != null) - .Select(x => x.Title?.Engines) - .ToList() - ?? new List(); - var engines = string.Join(",", allFoundEntriesEngines); - if (string.IsNullOrEmpty(engines)) + + var engines = parsedResponse?.CargoQuery?.FirstOrDefault()?.Title?.Engines; + if (engines == null) { - _logger.Info($"No engines found in response: {responseString}"); + _logger.Debug($"No engines found in response: {responseString}"); } return engines; From e297a3c114bf5584ad23b5aa272c8abfa2e67f47 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 11:55:22 +0300 Subject: [PATCH 09/14] Skipping when multiple entries found. --- .../GameEngineChecker/Services/PcGamingWikiClient.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs index 08460240f4..6c43536886 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs @@ -4,7 +4,6 @@ using Playnite.SDK; using Playnite.SDK.Models; using System; -using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; @@ -38,6 +37,13 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc response.EnsureSuccessStatusCode(); var parsedResponse = ParseResponse(responseString); + if (parsedResponse?.CargoQuery?.Count > 1) + { + var foundEntries = string.Join(", ", parsedResponse.CargoQuery.Select(x => x.Title?.Title)); + _logger.Info($"Multiple PC Gaming Wiki entries found for game {game.Name}: {foundEntries}. Skipping."); + return null; + } + var engines = parsedResponse?.CargoQuery?.FirstOrDefault()?.Title?.Engines; if (engines == null) { From 6e5846c9d9703af817ef29e47cd7c88f31dd5d18 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 20:08:26 +0300 Subject: [PATCH 10/14] Small logging improvements --- .../GameEngineChecker/Services/PcGamingWikiClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs index 6c43536886..1029bb9509 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs @@ -28,7 +28,7 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc try { var request = new HttpRequestMessage(HttpMethod.Get, link); - request.Headers.TryAddWithoutValidation("User-Agent", "Playnite.GameEngineChecker Extension 3.0 (https://github.com/SparrowBrain/)"); + request.Headers.TryAddWithoutValidation("User-Agent", "Playnite.GameEngineChecker Extension 3.x (https://github.com/SparrowBrain/)"); var response = await _httpClient.SendAsync(request, cancellationToken); var responseString = await response.Content.ReadAsStringAsync(); @@ -39,8 +39,8 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc if (parsedResponse?.CargoQuery?.Count > 1) { - var foundEntries = string.Join(", ", parsedResponse.CargoQuery.Select(x => x.Title?.Title)); - _logger.Info($"Multiple PC Gaming Wiki entries found for game {game.Name}: {foundEntries}. Skipping."); + var foundEntries = string.Join(", ", parsedResponse.CargoQuery.Select(x => $"\"{x.Title?.Title}\"")); + _logger.Info($"Multiple PC Gaming Wiki entries found for game {game.Id} - {game.Name}: {foundEntries}. Skipping."); return null; } From 5174d0ce26a90beb7eed4ba1dd62cea13ab256f5 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 20:17:11 +0300 Subject: [PATCH 11/14] Refactor --- .../GameEngineChecker/Services/RateLimiter.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/source/Generic/GameEngineChecker/Services/RateLimiter.cs b/source/Generic/GameEngineChecker/Services/RateLimiter.cs index d63aad9774..ad0206ecba 100644 --- a/source/Generic/GameEngineChecker/Services/RateLimiter.cs +++ b/source/Generic/GameEngineChecker/Services/RateLimiter.cs @@ -1,7 +1,7 @@ -using System; +using GameEngineChecker.Interfaces; +using System; using System.Threading; using System.Threading.Tasks; -using GameEngineChecker.Interfaces; namespace GameEngineChecker.Services { @@ -26,24 +26,9 @@ public async Task Limit(int batchSize, CancellationToken cancellationToken) { await _semaphore.WaitAsync(cancellationToken); - if (batchSize > _maxRequestsPerWindow) - { - var delayMilliseconds = _rateLimitWindow.TotalMilliseconds / _maxRequestsPerWindow; - await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds), cancellationToken); - } - - if (DateTime.UtcNow > ExecutionWindowEnd) - { - ResetWindow(DateTime.UtcNow); - } - - if (_totalExecutionInWindow >= _maxRequestsPerWindow) - { - var timeLeftInWindow = ExecutionWindowEnd - DateTime.UtcNow; - await Task.Delay(timeLeftInWindow, cancellationToken); - - ResetWindow(DateTime.UtcNow); - } + await SpreadExecutionsIfBatchLargerThanMaxPerWindow(batchSize, cancellationToken); + ResetWindowIfWindowEnded(); + await WaitForNewWindowIfMaxExecutionsReached(cancellationToken); _totalExecutionInWindow++; } @@ -55,9 +40,37 @@ public async Task Limit(int batchSize, CancellationToken cancellationToken) private DateTime ExecutionWindowEnd => _firstExecutionInWindow + _rateLimitWindow; - private void ResetWindow(DateTime utcNow) + private async Task SpreadExecutionsIfBatchLargerThanMaxPerWindow(int batchSize, CancellationToken cancellationToken) + { + if (batchSize > _maxRequestsPerWindow) + { + var delayMilliseconds = _rateLimitWindow.TotalMilliseconds / _maxRequestsPerWindow; + await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds), cancellationToken); + } + } + + private void ResetWindowIfWindowEnded() + { + if (DateTime.UtcNow > ExecutionWindowEnd) + { + ResetWindow(); + } + } + + private async Task WaitForNewWindowIfMaxExecutionsReached(CancellationToken cancellationToken) + { + if (_totalExecutionInWindow >= _maxRequestsPerWindow) + { + var timeLeftInWindow = ExecutionWindowEnd - DateTime.UtcNow; + await Task.Delay(timeLeftInWindow, cancellationToken); + + ResetWindow(); + } + } + + private void ResetWindow() { - _firstExecutionInWindow = utcNow; + _firstExecutionInWindow = DateTime.UtcNow; _totalExecutionInWindow = 0; } } From 231ce7ae1642cb3cd07a2a44aa9ea87b79cb28cf Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 22:01:29 +0300 Subject: [PATCH 12/14] Updated user-agent --- source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs index 1029bb9509..c218701b78 100644 --- a/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs +++ b/source/Generic/GameEngineChecker/Services/PcGamingWikiClient.cs @@ -28,7 +28,7 @@ public async Task GetEngines(Uri link, Game game, CancellationToken canc try { var request = new HttpRequestMessage(HttpMethod.Get, link); - request.Headers.TryAddWithoutValidation("User-Agent", "Playnite.GameEngineChecker Extension 3.x (https://github.com/SparrowBrain/)"); + request.Headers.TryAddWithoutValidation("User-Agent", "Playnite.GameEngineChecker Extension 3.x (https://github.com/darklinkpower/PlayniteExtensionsCollection/)"); var response = await _httpClient.SendAsync(request, cancellationToken); var responseString = await response.Content.ReadAsStringAsync(); From fb0392eb705050dca91d7ae66149f1551eaf0405 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 22:07:55 +0300 Subject: [PATCH 13/14] Added GameEngineChecker to extensions solution --- source/PlayniteExtensions.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/PlayniteExtensions.sln b/source/PlayniteExtensions.sln index 1e7262fd32..767ffbcfe3 100644 --- a/source/PlayniteExtensions.sln +++ b/source/PlayniteExtensions.sln @@ -129,6 +129,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "WebViewCore", "Common\WebVi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameEngineChecker.Tests", "GenericTests\GameEngineChecker.Tests\GameEngineChecker.Tests.csproj", "{AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameEngineChecker", "Generic\GameEngineChecker\GameEngineChecker.csproj", "{CA1D0577-BE3D-422D-808F-262D77A627DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -311,6 +313,10 @@ Global {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}.Release|Any CPU.Build.0 = Release|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -373,6 +379,7 @@ Global {B3A1FEFC-8C7F-4AA7-9993-B1D4F51222A9} = {82DA706F-BCA9-4D05-912F-E23A589A66C8} {5FBF6DC4-DFDB-4A07-8E73-0EF038F19E16} = {82DA706F-BCA9-4D05-912F-E23A589A66C8} {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1} = {A94A7FEF-3AA0-42E2-BA9F-A32BD1623476} + {CA1D0577-BE3D-422D-808F-262D77A627DF} = {0B392A79-5131-438C-BDB2-AEF170C06F60} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F739A97-4CBD-45FB-9A77-3D79008FD096} From 8bea57233eeabc144dfbe3101a24a61dbd483648 Mon Sep 17 00:00:00 2001 From: SparrowBrain Date: Sun, 24 May 2026 23:09:31 +0300 Subject: [PATCH 14/14] Project Guid updated --- .../GameEngineChecker/GameEngineChecker.csproj | 2 +- .../Generic/GameEngineChecker/GameEngineChecker.sln | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/source/Generic/GameEngineChecker/GameEngineChecker.csproj b/source/Generic/GameEngineChecker/GameEngineChecker.csproj index 195da981a0..2fa1312441 100644 --- a/source/Generic/GameEngineChecker/GameEngineChecker.csproj +++ b/source/Generic/GameEngineChecker/GameEngineChecker.csproj @@ -4,7 +4,7 @@ Debug AnyCPU - {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F} + {CA1D0577-BE3D-422D-808F-262D77A627DF} Library Properties GameEngineChecker diff --git a/source/Generic/GameEngineChecker/GameEngineChecker.sln b/source/Generic/GameEngineChecker/GameEngineChecker.sln index 470547910f..1bf3f30d47 100644 --- a/source/Generic/GameEngineChecker/GameEngineChecker.sln +++ b/source/Generic/GameEngineChecker/GameEngineChecker.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.37111.16 d17.14 +VisualStudioVersion = 17.14.37111.16 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameEngineChecker", "GameEngineChecker.csproj", "{4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameEngineChecker", "GameEngineChecker.csproj", "{CA1D0577-BE3D-422D-808F-262D77A627DF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameEngineChecker.Tests", "..\..\GenericTests\GameEngineChecker.Tests\GameEngineChecker.Tests.csproj", "{AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}" EndProject @@ -13,10 +13,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Release|Any CPU.Build.0 = Release|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA1D0577-BE3D-422D-808F-262D77A627DF}.Release|Any CPU.Build.0 = Release|Any CPU {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC1FED92-44D4-4527-8DE9-AF4120DB3EF1}.Release|Any CPU.ActiveCfg = Release|Any CPU