From e3046ae48e77e1a017d5b8f11e550b68c833c1f0 Mon Sep 17 00:00:00 2001 From: Lacro59 Date: Sun, 24 Oct 2021 22:46:44 +0200 Subject: [PATCH] Add Epic achievements #121 --- source/Clients/EpicAchievements.cs | 269 ++++++++++++++++++ source/Localization/LocSource.xaml | 5 +- source/Models/Achievements.cs | 5 + source/Models/EpicAchievementsData.cs | 71 +++++ source/Models/EpicAchievementsOwnedData.cs | 50 ++++ source/Models/EpicData.cs | 37 +++ source/Services/SuccessStoryDatabase.cs | 8 + source/SuccessStory.csproj | 4 + source/SuccessStorySettings.cs | 1 + source/Views/SuccessStorySettingsView.xaml | 43 ++- source/Views/SuccessStorySettingsView.xaml.cs | 12 + source/playnite-plugincommon | 2 +- 12 files changed, 499 insertions(+), 8 deletions(-) create mode 100644 source/Clients/EpicAchievements.cs create mode 100644 source/Models/EpicAchievementsData.cs create mode 100644 source/Models/EpicAchievementsOwnedData.cs create mode 100644 source/Models/EpicData.cs diff --git a/source/Clients/EpicAchievements.cs b/source/Clients/EpicAchievements.cs new file mode 100644 index 00000000..4d9cb105 --- /dev/null +++ b/source/Clients/EpicAchievements.cs @@ -0,0 +1,269 @@ +using CommonPlayniteShared.Common; +using CommonPlayniteShared.PluginLibrary.EpicLibrary; +using CommonPlayniteShared.PluginLibrary.EpicLibrary.Services; +using CommonPluginsShared; +using CommonPluginsShared.Models; +using Playnite.SDK.Data; +using Playnite.SDK.Models; +using SuccessStory.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace SuccessStory.Clients +{ + class EpicAchievements : GenericAchievements + { + protected static EpicAccountClient _EpicAPI; + internal static EpicAccountClient EpicAPI + { + get + { + if (_EpicAPI == null) + { + _EpicAPI = new EpicAccountClient( + PluginDatabase.PlayniteApi, + PluginDatabase.Paths.PluginUserDataPath + "\\..\\00000002-DBD1-46C6-B5D0-B1BA559D10E4\\tokens.json" + ); + } + return _EpicAPI; + } + + set + { + _EpicAPI = value; + } + } + + private const string UrlAchievements = @"https://www.epicgames.com/store/{0}/achievements/{1}"; + + + public EpicAchievements() : base("Epic", CodeLang.GetGogLang(PluginDatabase.PlayniteApi.ApplicationSettings.Language)) + { + + } + + + public override GameAchievements GetAchievements(Game game) + { + GameAchievements gameAchievements = SuccessStory.PluginDatabase.GetDefault(game); + List AllAchievements = new List(); + + + string Url = string.Empty; + string ResultWeb = string.Empty; + + + if (IsConnected()) + { + try + { + var tokens = EpicAPI.loadTokens(); + + string ProductSlug = GetProductSlug(game.Name); + Url = string.Format(UrlAchievements, LocalLang, ProductSlug); + + ResultWeb = Web.DownloadStringData(Url, tokens.access_token).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + if (ex.Message.Contains("404")) + { + logger.Warn($"Error 404 for {game.Name}"); + } + else + { + ShowNotificationPluginError(ex); + } + + return gameAchievements; + } + + if (ResultWeb != string.Empty && !ResultWeb.Contains("404", StringComparison.InvariantCultureIgnoreCase)) + { + try + { + int indexStart = ResultWeb.IndexOf("window.__REACT_QUERY_INITIAL_QUERIES__ ="); + int indexEnd = ResultWeb.IndexOf("window.server_rendered"); + + int length = ResultWeb.Length + - (indexStart + "window.__REACT_QUERY_INITIAL_QUERIES__ =".Length) + - (ResultWeb.Length - indexEnd); + + + string JsonDataString = ResultWeb.Substring( + indexStart + "window.__REACT_QUERY_INITIAL_QUERIES__ =".Length, + length + ); + + + indexEnd = JsonDataString.IndexOf(";"); + length = JsonDataString.Length - (JsonDataString.Length - indexEnd); + JsonDataString = JsonDataString.Substring(0, length); + + + EpicData epicData = Serialization.FromJson(JsonDataString); + + + // Achievements data + var achievemenstData = epicData.queries + .Where(x => (Serialization.ToJson(x.state.data)).Contains("\"achievements\":[{\"achievement\"", StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); + + EpicAchievementsData epicAchievementsData = Serialization.FromJson(Serialization.ToJson(achievemenstData.state.data)); + + if (epicAchievementsData != null && epicAchievementsData.Achievement.productAchievementsRecordBySandbox.achievements?.Count > 0) + { + foreach (var ach in epicAchievementsData.Achievement.productAchievementsRecordBySandbox.achievements) + { + Achievements temp = new Achievements + { + ApiName = ach.achievement.name, + Name = ach.achievement.unlockedDisplayName, + Description = ach.achievement.unlockedDescription, + UrlUnlocked = ach.achievement.unlockedIconLink, + UrlLocked = ach.achievement.lockedIconLink, + DateUnlocked = default(DateTime), + Percent = ach.achievement.rarity.percent + }; + + AllAchievements.Add(temp); + } + } + + // Owned achievement + var achievemenstOwnedData = epicData.queries + .Where(x => (Serialization.ToJson(x.state.data)).Contains("\"playerAchievements\":[{\"playerAchievement\"", StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); + + if (achievemenstOwnedData != null) + { + EpicAchievementsOwnedData epicAchievementsOwnedData = Serialization.FromJson(Serialization.ToJson(achievemenstOwnedData.state.data)); + + if (epicAchievementsOwnedData != null && epicAchievementsOwnedData.PlayerAchievement.playerAchievementGameRecordsBySandbox.records.FirstOrDefault()?.playerAchievements?.Count() > 0) + { + foreach (var ach in epicAchievementsOwnedData.PlayerAchievement.playerAchievementGameRecordsBySandbox.records.FirstOrDefault().playerAchievements) + { + var owned = AllAchievements.Find(x => x.ApiName == ach.playerAchievement.achievementName); + if (owned != null) + { + owned.DateUnlocked = ach.playerAchievement.unlockDate; + } + else + { + } + } + } + } + } + catch (Exception ex) + { + ShowNotificationPluginError(ex); + return gameAchievements; + } + } + else + { + logger.Warn($"Error 404 for {game.Name}"); + } + } + else + { + ShowNotificationPluginNoAuthenticate(resources.GetString("LOCSuccessStoryNotificationsGogNoAuthenticate")); + } + + + gameAchievements.Items = AllAchievements; + + + // Set source link + if (gameAchievements.HasAchivements) + { + gameAchievements.SourcesLink = new SourceLink + { + GameName = gameAchievements.Name, + Name = "Epic", + Url = Url + }; + } + + + return gameAchievements; + } + + + #region Configuration + public override bool ValidateConfiguration() + { + if (PlayniteTools.IsDisabledPlaynitePlugins("EpicLibrary")) + { + ShowNotificationPluginDisable(resources.GetString("LOCSuccessStoryNotificationsEpicDisabled")); + return false; + } + else + { + if (CachedConfigurationValidationResult == null) + { + CachedConfigurationValidationResult = IsConnected(); + + if (!(bool)CachedConfigurationValidationResult) + { + ShowNotificationPluginNoAuthenticate(resources.GetString("LOCSuccessStoryNotificationsEpicNoAuthenticate")); + } + } + else if (!(bool)CachedConfigurationValidationResult) + { + ShowNotificationPluginErrorMessage(); + } + + return (bool)CachedConfigurationValidationResult; + } + } + + + public override bool IsConnected() + { + if (CachedIsConnectedResult == null) + { + CachedIsConnectedResult = EpicAPI.GetIsUserLoggedIn(); + } + + return (bool)CachedIsConnectedResult; + } + + public override bool EnabledInSettings() + { + return PluginDatabase.PluginSettings.Settings.EnableEpic; + } + #endregion + + + #region Epic + private string GetProductSlug(string Name) + { + string ProductSlug = string.Empty; + + using (var client = new WebStoreClient()) + { + var catalogs = client.QuerySearch(Name).GetAwaiter().GetResult(); + if (catalogs.HasItems()) + { + var catalog = catalogs.FirstOrDefault(a => a.title.Equals(Name, StringComparison.InvariantCultureIgnoreCase)); + if (catalog == null) + { + catalog = catalogs[0]; + } + + ProductSlug = catalog.productSlug; + } + } + + return ProductSlug; + } + #endregion + } +} diff --git a/source/Localization/LocSource.xaml b/source/Localization/LocSource.xaml index cabe71c6..756aff6a 100644 --- a/source/Localization/LocSource.xaml +++ b/source/Localization/LocSource.xaml @@ -44,7 +44,9 @@ Battle.net user is not authenticated Battle.net Overwatch user is not authenticated Battle.net StarCraft II user is not authenticated - + Epic is enabled in SuccessStory but the library extension has been disabled in Playnite + Epic user is not authenticated + Achievements count @@ -65,6 +67,7 @@ Enable PSN achievements Enable Steam achievements Enable GOG achievements + Enable Epic achievements Enable Origin achievements Enable Xbox achievements Enable RetroAchievements achievements diff --git a/source/Models/Achievements.cs b/source/Models/Achievements.cs index d15c8b56..1e0a1bdc 100644 --- a/source/Models/Achievements.cs +++ b/source/Models/Achievements.cs @@ -270,6 +270,11 @@ private string GetNameFromUrl(string url) string NameFromUrl = string.Empty; List urlSplited = url.Split('/').ToList(); + if (url.IndexOf("epicgames.com") > -1) + { + NameFromUrl = "epic_" + Name.Replace(" ", "") + "_" + url.Substring(url.Length - 4).Replace(".png", string.Empty); + } + if (url.IndexOf(".playstation.") > -1) { NameFromUrl = "playstation_" + Name.Replace(" ", "") + "_" + url.Substring(url.Length - 4).Replace(".png", string.Empty); diff --git a/source/Models/EpicAchievementsData.cs b/source/Models/EpicAchievementsData.cs new file mode 100644 index 00000000..a2e222e6 --- /dev/null +++ b/source/Models/EpicAchievementsData.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SuccessStory.Models +{ + public class EpicAchievementsData + { + public AchievementEpic Achievement { get; set; } + } + + public class PlatinumRarity + { + public int percent { get; set; } + } + + public class Tier + { + public string name { get; set; } + public string hexColor { get; set; } + public int min { get; set; } + public int max { get; set; } + } + + public class Rarity + { + public int percent { get; set; } + } + + public class Achievement3 + { + public string sandboxId { get; set; } + public string deploymentId { get; set; } + public string name { get; set; } + public bool hidden { get; set; } + public string unlockedDisplayName { get; set; } + public string lockedDisplayName { get; set; } + public string unlockedDescription { get; set; } + public string lockedDescription { get; set; } + public string unlockedIconId { get; set; } + public string lockedIconId { get; set; } + public int XP { get; set; } + public string flavorText { get; set; } + public string unlockedIconLink { get; set; } + public string lockedIconLink { get; set; } + public Tier tier { get; set; } + public Rarity rarity { get; set; } + } + + public class Achievement2 + { + public Achievement3 achievement { get; set; } + } + + public class ProductAchievementsRecordBySandbox + { + public string productId { get; set; } + public string sandboxId { get; set; } + public int totalAchievements { get; set; } + public int totalProductXP { get; set; } + public PlatinumRarity platinumRarity { get; set; } + public List achievements { get; set; } + } + + public class AchievementEpic + { + public ProductAchievementsRecordBySandbox productAchievementsRecordBySandbox { get; set; } + } +} diff --git a/source/Models/EpicAchievementsOwnedData.cs b/source/Models/EpicAchievementsOwnedData.cs new file mode 100644 index 00000000..a7d56176 --- /dev/null +++ b/source/Models/EpicAchievementsOwnedData.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SuccessStory.Models +{ + class EpicAchievementsOwnedData + { + public PlayerAchievement PlayerAchievement { get; set; } + } + + public class PlayerAchievement3 + { + public string sandboxId { get; set; } + public string epicAccountId { get; set; } + public bool unlocked { get; set; } + public int progress { get; set; } + public int XP { get; set; } + public DateTime unlockDate { get; set; } + public string achievementName { get; set; } + } + + public class PlayerAchievement2 + { + public PlayerAchievement3 playerAchievement { get; set; } + } + + public class Record + { + public int totalXP { get; set; } + public int totalUnlocked { get; set; } + public List playerAchievements { get; set; } + } + + public class PlayerAchievementGameRecordsBySandbox + { + public List records { get; set; } + } + + public class PlayerAchievement + { + public PlayerAchievementGameRecordsBySandbox playerAchievementGameRecordsBySandbox { get; set; } + } + + + + +} diff --git a/source/Models/EpicData.cs b/source/Models/EpicData.cs new file mode 100644 index 00000000..086d80b3 --- /dev/null +++ b/source/Models/EpicData.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SuccessStory.Models +{ + public class EpicData + { + public List mutations { get; set; } + public List queries { get; set; } + } + + public class State + { + public object data { get; set; } + public int dataUpdateCount { get; set; } + public object dataUpdatedAt { get; set; } + public object error { get; set; } + public int errorUpdateCount { get; set; } + public int errorUpdatedAt { get; set; } + public int fetchFailureCount { get; set; } + public object fetchMeta { get; set; } + public bool isFetching { get; set; } + public bool isInvalidated { get; set; } + public bool isPaused { get; set; } + public string status { get; set; } + } + + public class Query + { + public State state { get; set; } + public object queryKey { get; set; } + public string queryHash { get; set; } + } +} diff --git a/source/Services/SuccessStoryDatabase.cs b/source/Services/SuccessStoryDatabase.cs index 4a85867d..7d10857f 100644 --- a/source/Services/SuccessStoryDatabase.cs +++ b/source/Services/SuccessStoryDatabase.cs @@ -42,6 +42,7 @@ public class SuccessStoryDatabase : PluginDatabaseObject { { AchievementSource.GOG, new GogAchievements() }, + { AchievementSource.Epic, new EpicAchievements() }, { AchievementSource.Origin, new OriginAchievements() }, { AchievementSource.Overwatch, new OverwatchAchievements() }, { AchievementSource.Playstation, new PSNAchievements() }, @@ -704,6 +705,7 @@ public enum AchievementSource Playstation, Steam, GOG, + Epic, Origin, Xbox, RetroAchievements, @@ -746,6 +748,12 @@ private static AchievementSource GetAchievementSourceFromLibraryPlugin(SuccessSt return AchievementSource.GOG; } break; + case ExternalPlugin.EpicLibrary: + if (settings.EnableEpic) + { + return AchievementSource.Epic; + } + break; case ExternalPlugin.OriginLibrary: if (settings.EnableOrigin) { diff --git a/source/SuccessStory.csproj b/source/SuccessStory.csproj index 0760da08..4b087f21 100644 --- a/source/SuccessStory.csproj +++ b/source/SuccessStory.csproj @@ -113,6 +113,7 @@ + @@ -157,6 +158,9 @@ + + + diff --git a/source/SuccessStorySettings.cs b/source/SuccessStorySettings.cs index 839cb822..6da2036a 100644 --- a/source/SuccessStorySettings.cs +++ b/source/SuccessStorySettings.cs @@ -183,6 +183,7 @@ public bool EnableIntegrationList public bool EnablePsn { get; set; } = false; public bool EnableSteam { get; set; } = false; public bool EnableGog { get; set; } = false; + public bool EnableEpic { get; set; } = false; public bool EnableOrigin { get; set; } = false; public bool EnableXbox { get; set; } = false; public bool EnableRetroAchievements { get; set; } = false; diff --git a/source/Views/SuccessStorySettingsView.xaml b/source/Views/SuccessStorySettingsView.xaml index 7f539ed2..bcd6e723 100644 --- a/source/Views/SuccessStorySettingsView.xaml +++ b/source/Views/SuccessStorySettingsView.xaml @@ -36,6 +36,8 @@ + + @@ -202,6 +204,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -225,7 +256,7 @@ - +