From 33aeddf7fb647f0f670311d21708707702214c05 Mon Sep 17 00:00:00 2001 From: Jeshibu Date: Sun, 18 Feb 2024 20:53:11 +0100 Subject: [PATCH 1/4] Fixed 2 games both with empty collections resulting in 0 similarity --- .../GameRelationsBase.xaml.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs b/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs index 1ac8a34e57..c84b89f4b0 100644 --- a/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs +++ b/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs @@ -9,19 +9,10 @@ using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using System.Windows.Threading; -using TemporaryCache; namespace GameRelations.PlayniteControls { @@ -213,7 +204,7 @@ protected static decimal CalculateListHashSetMatchPercentage(IEnumerable l } // Rounding is done to prevent errors when doing arithmetic operations - var matchPercent = Math.Round(commonCount / (decimal)Math.Max(listToMatch.Count(), hashSet.Count), 3); + var matchPercent = Math.Round(commonCount / (decimal)Math.Max(listToMatch.Count(), hashSet.Count), 3); return matchPercent; } @@ -226,8 +217,14 @@ protected static decimal CalculateListHashSetMatchPercentage(IEnumerable l /// A match value between 0 and 1 representing the similarity between the elements. protected static double CalculateJaccardSimilarity(IEnumerable listToMatch, HashSet hashSet) { - if (listToMatch is null || !listToMatch.Any() || hashSet is null || hashSet.Count == 0) + if (listToMatch is null || !listToMatch.Any()) { + //If both games have 0 items in this collection, give some pity similarity + if (hashSet is null || hashSet.Count == 0) + { + return 0.9D; + } + return 0; } From 0822b83b0feeed629aa601a8ee4d62ce26c2869e Mon Sep 17 00:00:00 2001 From: Jeshibu Date: Sun, 25 Feb 2024 12:09:38 +0100 Subject: [PATCH 2/4] Rework: ignore fields if both games don't have items in them --- .../GameRelationsBase.xaml.cs | 10 +- .../PlayniteControls/SimilarGamesControl.cs | 112 +++++++++++++----- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs b/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs index c84b89f4b0..e4e4d5a45d 100644 --- a/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs +++ b/source/Generic/GameRelations/PlayniteControls/GameRelationsBase.xaml.cs @@ -217,14 +217,8 @@ protected static decimal CalculateListHashSetMatchPercentage(IEnumerable l /// A match value between 0 and 1 representing the similarity between the elements. protected static double CalculateJaccardSimilarity(IEnumerable listToMatch, HashSet hashSet) { - if (listToMatch is null || !listToMatch.Any()) + if (listToMatch is null || !listToMatch.Any() || hashSet is null || hashSet.Count == 0) { - //If both games have 0 items in this collection, give some pity similarity - if (hashSet is null || hashSet.Count == 0) - { - return 0.9D; - } - return 0; } @@ -281,7 +275,7 @@ protected static T GetAnyCommonItem(List listToMatch, HashSet hashSet) /// Returns false if the list is null or empty. protected static bool HashSetContainsAnyItem(IEnumerable listToMatch, HashSet hashSet) { - if (listToMatch is null || !listToMatch.Any()) + if (listToMatch is null) { return false; } diff --git a/source/Generic/GameRelations/PlayniteControls/SimilarGamesControl.cs b/source/Generic/GameRelations/PlayniteControls/SimilarGamesControl.cs index 1c5ff9f4a7..865ecc460b 100644 --- a/source/Generic/GameRelations/PlayniteControls/SimilarGamesControl.cs +++ b/source/Generic/GameRelations/PlayniteControls/SimilarGamesControl.cs @@ -1,70 +1,55 @@ -using GameRelations.Interfaces; -using GameRelations.Models; +using GameRelations.Models; using Playnite.SDK; using Playnite.SDK.Models; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media.Imaging; -using TemporaryCache; namespace GameRelations.PlayniteControls { public partial class SimilarGamesControl : GameRelationsBase { - private readonly Dictionary _propertiesWeights; - private const double _minMatchValueFactor = 0.73; + private readonly Dictionary _propertiesWeights; + private const double _minMatchValueFactor = 0.69; private readonly SimilarGamesControlSettings _controlSettings; public SimilarGamesControl(IPlayniteAPI playniteApi, GameRelationsSettings settings, SimilarGamesControlSettings controlSettings) : base(playniteApi, settings, controlSettings) { _controlSettings = controlSettings; - _propertiesWeights = new Dictionary + _propertiesWeights = new Dictionary { - {"tags", 1 }, - {"genres", 1.2 }, - {"categories", 1.3 } + { GameField.TagIds, 1 }, + { GameField.GenreIds, 1.2 }, + { GameField.CategoryIds, 1.3 } }; } public override IEnumerable GetMatchingGames(Game game) { - var tagsSet = GetItemsNotInHashSet(game.TagIds, _controlSettings.TagsToIgnore).ToHashSet(); - var genresSet = GetItemsNotInHashSet(game.GenreIds, _controlSettings.GenresToIgnore).ToHashSet(); - var categoriesSet = GetItemsNotInHashSet(game.CategoryIds, _controlSettings.CategoriesToIgnore).ToHashSet(); + var gtm = GetGameToMatchInfo(game); - var seriesHashSet = game.SeriesIds?.ToHashSet() ?? new HashSet(); - - var minScoreThreshold = _propertiesWeights.Count * _minMatchValueFactor; var similarityScores = new Dictionary(); foreach (var otherGame in PlayniteApi.Database.Games) { - if (otherGame.Id == game.Id) + if (otherGame.Id == gtm.Game.Id) { continue; } - if (!game.Hidden && otherGame.Hidden) + if (!gtm.Game.Hidden && otherGame.Hidden) { continue; } - if (_controlSettings.ExcludeGamesSameSeries && HashSetContainsAnyItem(otherGame.SeriesIds, seriesHashSet)) + if (_controlSettings.ExcludeGamesSameSeries && HashSetContainsAnyItem(otherGame.SeriesIds, gtm.SeriesIds)) { continue; } - var tagsScore = CalculateJaccardSimilarity(GetItemsNotInHashSet(otherGame.TagIds, _controlSettings.TagsToIgnore), tagsSet) * _propertiesWeights["tags"]; - var genresScore = CalculateJaccardSimilarity(GetItemsNotInHashSet(otherGame.GenreIds, _controlSettings.GenresToIgnore), genresSet) * _propertiesWeights["genres"]; - var categoriesScore = CalculateJaccardSimilarity(GetItemsNotInHashSet(otherGame.CategoryIds, _controlSettings.CategoriesToIgnore), categoriesSet) * _propertiesWeights["categories"]; - - var finalScore = tagsScore + genresScore + categoriesScore; - if (finalScore >= minScoreThreshold) + if (IsSimilar(gtm, otherGame, out double similarity)) { - similarityScores.Add(otherGame, finalScore); + similarityScores.Add(otherGame, similarity); } } @@ -73,5 +58,76 @@ public override IEnumerable GetMatchingGames(Game game) return similarGames; } + + private class GameToMatchInfo + { + public Game Game { get; set; } + public Dictionary> FilteredValues { get; set; } + public HashSet SeriesIds { get; set; } + + public GameToMatchInfo(Game game) + { + Game = game; + SeriesIds = game.SeriesIds?.ToHashSet() ?? new HashSet(); + FilteredValues = new Dictionary>(); + } + } + + private GameToMatchInfo GetGameToMatchInfo(Game game) + { + var gtm = new GameToMatchInfo(game); + foreach (var field in _propertiesWeights.Keys) + { + gtm.FilteredValues[field] = GetFilteredValue(game, field).ToHashSet(); + } + return gtm; + } + + protected HashSet GetItemsToIgnore(GameField field) + { + switch (field) + { + case GameField.TagIds: return _controlSettings.TagsToIgnore; + case GameField.GenreIds: return _controlSettings.GenresToIgnore; + case GameField.CategoryIds: return _controlSettings.CategoriesToIgnore; + default: return new HashSet(); + } + } + + private static List GetFieldValue(Game game, GameField field) + { + switch (field) + { + case GameField.TagIds: return game.TagIds; + case GameField.GenreIds: return game.GenreIds; + case GameField.CategoryIds: return game.CategoryIds; + default: return new List(); + } + } + + private IEnumerable GetFilteredValue(Game game, GameField field) + { + return GetItemsNotInHashSet(GetFieldValue(game, field) ?? new List(), GetItemsToIgnore(field)); + } + + private bool IsSimilar(GameToMatchInfo gtm, Game otherGame, out double similarity) + { + double matchThreshold = 0; + similarity = 0; + + foreach (var property in gtm.FilteredValues) + { + var otherValues = GetFilteredValue(otherGame, property.Key); + if (!property.Value.Any() && !otherValues.Any()) + { + continue; + } + + matchThreshold += _minMatchValueFactor; + similarity += CalculateJaccardSimilarity(otherValues, property.Value) * _propertiesWeights[property.Key]; + } + + return matchThreshold > 0 && similarity > matchThreshold; + } } } \ No newline at end of file From 2b016f553569c12832b1d7f8007aa41d9c85e19c Mon Sep 17 00:00:00 2001 From: Jeshibu Date: Sat, 23 Mar 2024 11:45:20 +0100 Subject: [PATCH 3/4] Aded settings --- .../GameRelations/GameRelationsSettings.cs | 30 ++++++++- .../GameRelationsSettingsView.xaml | 43 +++++++++++-- .../GameRelationsSettingsView.xaml.cs | 16 +---- .../Models/SimilarGamesControlSettings.cs | 41 ++++++++++-- .../PlayniteControls/SimilarGamesControl.cs | 64 +++++++++---------- 5 files changed, 136 insertions(+), 58 deletions(-) diff --git a/source/Generic/GameRelations/GameRelationsSettings.cs b/source/Generic/GameRelations/GameRelationsSettings.cs index 40edb563ce..b30bc621a1 100644 --- a/source/Generic/GameRelations/GameRelationsSettings.cs +++ b/source/Generic/GameRelations/GameRelationsSettings.cs @@ -27,6 +27,8 @@ public class GameRelationsSettings : ObservableObject private SimilarGamesControlSettings similarGamesControlSettings = new SimilarGamesControlSettings(); public SimilarGamesControlSettings SimilarGamesControlSettings { get => similarGamesControlSettings; set => SetValue(ref similarGamesControlSettings, value); } + + public int SettingsVersion = 1; } public class GameRelationsSettingsViewModel : ObservableObject, ISettings @@ -72,10 +74,12 @@ public GameRelationsSettingsViewModel(GameRelations plugin) if (savedSettings != null) { Settings = savedSettings; + UpgradeSettings(); } else { Settings = new GameRelationsSettings(); + SetAdvancedSectionDefaults(); } } @@ -185,6 +189,27 @@ private void RemoveSelectedItemsFromExclude(ObservableCollection } } + private string GetResourceString(string key) => plugin.PlayniteApi.Resources.GetString(key); + + public void SetAdvancedSectionDefaults() + { + Settings.SimilarGamesControlSettings.JacardSimilarityPerField = 0.73D; + Settings.SimilarGamesControlSettings.FieldSettings.Clear(); + settings.SimilarGamesControlSettings.FieldSettings.Add(new SimilarGamesFieldSettings(GameField.TagIds, GetResourceString("LOCTagsLabel"), true, 1)); + settings.SimilarGamesControlSettings.FieldSettings.Add(new SimilarGamesFieldSettings(GameField.GenreIds, GetResourceString("LOCGenresLabel"), true, 1.2)); + settings.SimilarGamesControlSettings.FieldSettings.Add(new SimilarGamesFieldSettings(GameField.CategoryIds, GetResourceString("LOCCategoriesLabel"), true, 1.3)); + } + + public void UpgradeSettings() + { + int currentVersion = 2; + + if (settings.SettingsVersion < 2) + SetAdvancedSectionDefaults(); + + Settings.SettingsVersion = currentVersion; + } + public RelayCommand AddSelectedTagsToExcludeCommand { get => new RelayCommand(() => @@ -233,6 +258,9 @@ public RelayCommand RemoveSelectedCategoriesFromExcludeCommand }, () => SgExcludeCategoriesSelectedItems.Count > 0); } - + public RelayCommand SetAdvancedSectionDefaultsCommand + { + get => new RelayCommand(SetAdvancedSectionDefaults); + } } } \ No newline at end of file diff --git a/source/Generic/GameRelations/GameRelationsSettingsView.xaml b/source/Generic/GameRelations/GameRelationsSettingsView.xaml index 7efe8c7c52..53620345e6 100644 --- a/source/Generic/GameRelations/GameRelationsSettingsView.xaml +++ b/source/Generic/GameRelations/GameRelationsSettingsView.xaml @@ -41,7 +41,7 @@ - + - + - + @@ -142,7 +142,7 @@ ItemsSource="{Binding SimilarGamesNotExcludeTags}" SelectionChanged="SimilarGamesNotExcludeTagsLb_SelectionChanged" /> - +