├── Source ├── icon.png ├── Resources │ ├── DefaultSteamShortcutIcon.png │ └── DefaultTarget.json ├── packages.config ├── App.xaml ├── Models │ ├── SteamLauncher │ │ ├── ISteamMode.cs │ │ ├── SteamBigPictureMode.cs │ │ ├── Steam.cs │ │ └── SteamDesktopMode.cs │ ├── Overlays │ │ ├── Types │ │ │ ├── DefaultGameOverlay.cs │ │ │ ├── PlayniteOverlay.cs │ │ │ ├── SteamStartableOverlay.cs │ │ │ ├── ExternallyStartedOverlay.cs │ │ │ ├── Overlay.cs │ │ │ └── GameOverlay.cs │ │ └── GlosSITargetProcess.cs │ ├── GlosSITargets │ │ ├── Types │ │ │ ├── GlosSITarget.cs │ │ │ ├── UnidentifiedGlosSITarget.cs │ │ │ ├── PlayniteGlosSITarget.cs │ │ │ ├── GameGlosSITarget.cs │ │ │ └── DefaultGlosSITarget.cs │ │ ├── KnownTargets.cs │ │ ├── Files │ │ │ ├── StartFromSteamLaunchOptions.cs │ │ │ ├── GlosSITargetFileInfo.cs │ │ │ ├── GameGlosSITargetFile.cs │ │ │ ├── JsonExtensions.cs │ │ │ └── GlosSITargetSettings.cs │ │ ├── Shortcuts │ │ │ ├── GlosSISteamShortcut.cs │ │ │ ├── SteamShortcut.cs │ │ │ └── Crc.cs │ │ └── TargetsVersionMigrator.cs │ ├── HardLink.cs │ ├── ProcessExtensions.cs │ ├── PlayniteGameSteamAssets.cs │ ├── SteamGameAssets.cs │ └── WinWindow.cs ├── extension.yaml ├── GlosSIIntegration.sln ├── Views │ ├── ShortcutCreationView.xaml.cs │ ├── GlosSIIntegrationSettingsView.xaml.cs │ ├── ShortcutCreationView.xaml │ └── GlosSIIntegrationSettingsView.xaml ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Scripts │ └── StartPlayniteFromGlosSI.vbs ├── GlosSIIntegrationSettings.cs ├── GlosSIIntegration.csproj └── Localization │ └── loc_source.xaml ├── crowdin.yml ├── Screenshots ├── AddonSettingsDefault.png ├── AddonSettingsHelium.png ├── OverlayShortcutCreationDefault.png ├── OverlayShortcutCreationHelium.png └── Thumbnails │ └── AddonSettingsDefault.jpg ├── .gitattributes ├── README.md ├── InstallerManifest.yaml └── .gitignore /Source/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Source/icon.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /Source/Localization/loc_source.xaml 3 | translation: /Source/Localization/%locale_with_underscore%.xaml -------------------------------------------------------------------------------- /Screenshots/AddonSettingsDefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Screenshots/AddonSettingsDefault.png -------------------------------------------------------------------------------- /Screenshots/AddonSettingsHelium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Screenshots/AddonSettingsHelium.png -------------------------------------------------------------------------------- /Screenshots/OverlayShortcutCreationDefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Screenshots/OverlayShortcutCreationDefault.png -------------------------------------------------------------------------------- /Screenshots/OverlayShortcutCreationHelium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Screenshots/OverlayShortcutCreationHelium.png -------------------------------------------------------------------------------- /Source/Resources/DefaultSteamShortcutIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Source/Resources/DefaultSteamShortcutIcon.png -------------------------------------------------------------------------------- /Screenshots/Thumbnails/AddonSettingsDefault.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/HEAD/Screenshots/Thumbnails/AddonSettingsDefault.jpg -------------------------------------------------------------------------------- /Source/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Source/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/Models/SteamLauncher/ISteamMode.cs: -------------------------------------------------------------------------------- 1 | namespace GlosSIIntegration.Models.SteamLauncher 2 | { 3 | /// 4 | /// Represents the currently running mode of Steam. 5 | /// Note that it can become invalid at any time: if the user changes mode or exits Steam. 6 | /// 7 | internal interface ISteamMode 8 | { 9 | /// 10 | /// The main Steam window. 11 | /// 12 | WinWindow MainWindow { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /Source/Models/Overlays/Types/DefaultGameOverlay.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Types; 2 | using Playnite.SDK.Models; 3 | 4 | namespace GlosSIIntegration.Models.Overlays.Types 5 | { 6 | /// 7 | /// Represents an used by default for Playnite games 8 | /// without a specific overlay. 9 | /// 10 | internal class DefaultGameOverlay : GameOverlay 11 | { 12 | public DefaultGameOverlay(Game associatedGame) : base(associatedGame, new DefaultGlosSITarget()) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/Resources/DefaultTarget.json: -------------------------------------------------------------------------------- 1 | { 2 | "controller": { 3 | "allowDesktopConfig": false, 4 | "emulateDS4": false, 5 | "maxControllers": 4 6 | }, 7 | "devices": { 8 | "hideDevices": true, 9 | "realDeviceIds": false 10 | }, 11 | "extendedLogging": false, 12 | "snapshotNotify": false, 13 | "version": 1, 14 | "window": { 15 | "disableOverlay": false, 16 | "hideAltTab": true, 17 | "maxFps": null, 18 | "scale": null, 19 | "windowMode": false, 20 | "disableGlosSIOverlay": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/extension.yaml: -------------------------------------------------------------------------------- 1 | Id: GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315 2 | Name: GlosSI Integration 3 | Author: LemmusLemmus 4 | Version: 1.2.2 5 | Module: GlosSIIntegration.dll 6 | Type: GenericPlugin 7 | Icon: icon.png 8 | Links: 9 | - Name: GitHub 10 | Url: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite 11 | - Name: Wiki 12 | Url: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/wiki 13 | - Name: Translate 14 | Url: https://crowdin.com/project/glossi-integration-playnite 15 | - Name: Playnite Forum 16 | Url: https://playnite.link/forum/thread-1307.html -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Types/GlosSITarget.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Files; 2 | using GlosSIIntegration.Models.GlosSITargets.Shortcuts; 3 | 4 | namespace GlosSIIntegration.Models.GlosSITargets.Types 5 | { 6 | internal abstract class GlosSITarget : GlosSISteamShortcut // TODO: Composition instead of inheritance! Makes more sense as well. 7 | { 8 | public GlosSITargetFile File { get; } 9 | 10 | protected GlosSITarget(string name) : base(name) 11 | { 12 | File = GetGlosSITargetFile(); 13 | } 14 | 15 | protected virtual GlosSITargetFile GetGlosSITargetFile() 16 | { 17 | return new GlosSITargetFile(this); 18 | } 19 | 20 | protected internal abstract GlosSITargetSettings.LaunchOptions GetPreferredLaunchOptions(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Types/UnidentifiedGlosSITarget.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Files; 2 | using System; 3 | 4 | namespace GlosSIIntegration.Models.GlosSITargets.Types 5 | { 6 | /// 7 | /// An unidentified type of GlosSITarget. It could be unrelated to this extension. 8 | /// 9 | internal class UnidentifiedGlosSITarget : GlosSITarget 10 | { 11 | public UnidentifiedGlosSITarget(string name) : base(name) { } 12 | 13 | protected internal override GlosSITargetSettings.LaunchOptions GetPreferredLaunchOptions() 14 | { 15 | // Does not really adhere to the Liskov substitution principle, 16 | // but this overlay should really not be used for creating and verifing GlosSITargetFiles, 17 | // since its type is unknown. 18 | throw new NotSupportedException("The preferred launch options of the GlosSITarget is unknown."); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Types/PlayniteGlosSITarget.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Files; 2 | using System; 3 | 4 | namespace GlosSIIntegration.Models.GlosSITargets.Types 5 | { 6 | /// 7 | /// Represents a GlosSITarget used while browsing the Playnite library (in fullscreen mode). 8 | /// 9 | internal class PlayniteGlosSITarget : GlosSITarget 10 | { 11 | public PlayniteGlosSITarget(string name) : base(name) { } 12 | public PlayniteGlosSITarget() : base( 13 | GlosSIIntegration.GetSettings().PlayniteOverlayName ?? 14 | throw new NotSupportedException("PlayniteOverlayName setting not set.")) { } 15 | 16 | public static bool Exists() 17 | { 18 | return !string.IsNullOrEmpty(GlosSIIntegration.GetSettings().PlayniteOverlayName); 19 | } 20 | 21 | protected internal override GlosSITargetSettings.LaunchOptions GetPreferredLaunchOptions() 22 | { 23 | return StartFromSteamLaunchOptions.GetLaunchPlayniteLibraryOptions(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Types/GameGlosSITarget.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Files; 2 | using Playnite.SDK.Models; 3 | 4 | namespace GlosSIIntegration.Models.GlosSITargets.Types 5 | { 6 | /// 7 | /// Represents a GlosSITarget used for a specific Playnite game. 8 | /// 9 | internal class GameGlosSITarget : GlosSITarget 10 | { 11 | public Game AssociatedGame { get; } 12 | 13 | public GameGlosSITarget(Game game) : base(game.Name) 14 | { 15 | AssociatedGame = game; 16 | } 17 | 18 | protected override GlosSITargetFile GetGlosSITargetFile() 19 | { 20 | return new GameGlosSITargetFile(this); 21 | } 22 | 23 | protected internal override GlosSITargetSettings.LaunchOptions GetPreferredLaunchOptions() 24 | { 25 | return GetPreferredLaunchOptions(AssociatedGame); 26 | } 27 | 28 | private static GlosSITargetSettings.LaunchOptions GetPreferredLaunchOptions(Game game) 29 | { 30 | return StartFromSteamLaunchOptions.GetLaunchGameOptions(game); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Source/GlosSIIntegration.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31025.194 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GlosSIIntegration", "GlosSIIntegration.csproj", "{4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3EE16424-E313-474F-90E0-978AE3852083} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Source/Views/ShortcutCreationView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace GlosSIIntegration 5 | { 6 | /// 7 | /// Interaction logic for ShortcutCreationView.xaml 8 | /// 9 | public partial class ShortcutCreationView : UserControl 10 | { 11 | private readonly ShortcutCreationViewModel shortcutCreationModel; 12 | 13 | internal ShortcutCreationView(ShortcutCreationViewModel viewModel) 14 | { 15 | InitializeComponent(); 16 | viewModel.SetIconPreview(IconPreview); 17 | shortcutCreationModel = viewModel; 18 | DataContext = shortcutCreationModel; 19 | } 20 | 21 | /// 22 | /// Opens a link to the "Configuring the overlay" section on the GitHub wiki. 23 | /// 24 | private void Help_Click(object sender, RoutedEventArgs e) 25 | { 26 | GlosSIIntegrationSettingsViewModel.OpenLink("https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/wiki/Getting-started#configuring-the-overlay"); 27 | } 28 | 29 | private void Save_Click(object sender, RoutedEventArgs e) 30 | { 31 | if (shortcutCreationModel.Create()) 32 | { 33 | Window.GetWindow(this).DialogResult = true; 34 | Window.GetWindow(this).Close(); 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/KnownTargets.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.IO; 3 | 4 | namespace GlosSIIntegration.Models 5 | { 6 | // TODO: Use this class to keep track of created targets (and related Playnite tags). 7 | // For now, it is only used for version migration. 8 | [JsonObject(MemberSerialization.OptIn)] 9 | internal class KnownTargets 10 | { 11 | private const int CurrentVersion = 1; 12 | [JsonProperty] 13 | public int Version { get; } 14 | 15 | private KnownTargets() 16 | { 17 | // Note: When deserializing, do not set this property. 18 | Version = CurrentVersion; 19 | } 20 | 21 | public static void LoadTargets() 22 | { 23 | if (!File.Exists(GlosSIIntegration.GetSettings().KnownTargetsPath)) 24 | { 25 | TargetsVersionMigrator.TryMigrate(0); 26 | new KnownTargets().Save(); 27 | } 28 | else 29 | { 30 | // Next time migration is neccessary, call TryMigrate here with the deserialized Version. 31 | } 32 | } 33 | 34 | private void Save() 35 | { 36 | using (StreamWriter file = File.CreateText(GlosSIIntegration.GetSettings().KnownTargetsPath)) 37 | { 38 | new JsonSerializer().Serialize(file, this); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Models/HardLink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace GlosSIIntegration.Models 7 | { 8 | internal class HardLink 9 | { 10 | #region Win32 11 | [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 12 | private static extern bool CreateHardLink(string fileName, string existingFileName, IntPtr lpSecurityAttributes); 13 | 14 | private const string ExtendMaxPathLimitPrefix = @"\\?\"; 15 | #endregion Win32 16 | 17 | private static string GetExtendedPath(string path) 18 | { 19 | return ExtendMaxPathLimitPrefix + Path.GetFullPath(path); 20 | } 21 | 22 | /// 23 | /// Creates a hard link from one file to another. 24 | /// 25 | /// The path of the new file. 26 | /// Note that all directories in the path must already exist. 27 | /// The path to the file to make a hard link from. 28 | /// If unable to create the hard link. 29 | public static void Create(string toPath, string fromPath) 30 | { 31 | if (!CreateHardLink(GetExtendedPath(toPath), GetExtendedPath(fromPath), IntPtr.Zero)) 32 | { 33 | throw new Win32Exception(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("GlosSIIntegration")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("GlosSIIntegration")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("6b0297da-75e5-4330-bb2d-b64bff22c315")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Files/StartFromSteamLaunchOptions.cs: -------------------------------------------------------------------------------- 1 | using Playnite.SDK.Models; 2 | using System; 3 | using System.IO; 4 | 5 | namespace GlosSIIntegration.Models.GlosSITargets.Files 6 | { 7 | /// 8 | /// Represents GlosSITarget launch options for launching the StartPlayniteFromGlosSI.vbs script, 9 | /// used to launch Playnite games or the Playnite library from Steam. 10 | /// 11 | internal class StartFromSteamLaunchOptions : GlosSITargetSettings.LaunchOptions 12 | { 13 | private static readonly string wscriptPath = Path.Combine(Environment.SystemDirectory, "wscript.exe"); 14 | private static readonly string scriptArgument = $@"""{GlosSIIntegration.GetSettings().StartPlayniteFromGlosSIScriptPath}"""; 15 | 16 | private StartFromSteamLaunchOptions(string launchAppArgs) : base() 17 | { 18 | Launch = true; 19 | LaunchPath = wscriptPath; 20 | LaunchAppArgs = launchAppArgs; 21 | } 22 | 23 | public static StartFromSteamLaunchOptions GetLaunchPlayniteLibraryOptions() 24 | { 25 | return new StartFromSteamLaunchOptions(scriptArgument); 26 | } 27 | 28 | public static StartFromSteamLaunchOptions GetLaunchGameOptions(Game game) 29 | { 30 | return new StartFromSteamLaunchOptions($"{scriptArgument} {game.Id}"); 31 | } 32 | 33 | public static bool LaunchesPlaynite(GlosSITargetSettings.LaunchOptions launchOptions) 34 | { 35 | return launchOptions.LaunchPath == wscriptPath && launchOptions.LaunchAppArgs == scriptArgument; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Files/GlosSITargetFileInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace GlosSIIntegration.Models.GlosSITargets.Files 4 | { 5 | internal class GlosSITargetFileInfo 6 | { 7 | /// 8 | /// The filename of the .json GlosSITarget profile, without the ".json" file extension. 9 | /// 10 | public string Name { get; } 11 | public string FullPath { get; } 12 | 13 | public GlosSITargetFileInfo(string targetName) 14 | { 15 | Name = RemoveIllegalFileNameChars(targetName); 16 | FullPath = GetJsonFilePath(Name); 17 | } 18 | 19 | /// 20 | /// Checks if this object has a corresponding .json file. 21 | /// The actual name stored inside the .json file is not compared. 22 | /// 23 | /// true if the target has a corresponding .json file; false otherwise. 24 | public bool Exists() 25 | { 26 | return File.Exists(FullPath); 27 | } 28 | 29 | private static string RemoveIllegalFileNameChars(string filename) 30 | { 31 | if (filename == null) return null; 32 | return string.Concat(filename.Split(Path.GetInvalidFileNameChars())); 33 | } 34 | 35 | /// 36 | /// Gets the path to the .json with the supplied name. 37 | /// 38 | /// The name of the .json file. 39 | /// The path to the .json file. 40 | private static string GetJsonFilePath(string jsonFileName) 41 | { 42 | return Path.Combine(GlosSIIntegration.GetSettings().GlosSITargetsPath, jsonFileName + ".json"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Types/DefaultGlosSITarget.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Files; 2 | using System; 3 | 4 | namespace GlosSIIntegration.Models.GlosSITargets.Types 5 | { 6 | /// 7 | /// Represents a GlosSITarget used by default for Playnite games 8 | /// without a specific GlosSITarget. 9 | /// 10 | internal class DefaultGlosSITarget : GlosSITarget 11 | { 12 | public DefaultGlosSITarget(string name) : base(name) { } 13 | 14 | public DefaultGlosSITarget() : base( 15 | GlosSIIntegration.GetSettings().DefaultOverlayName ?? 16 | throw new NotSupportedException("DefaultOverlayName setting not set.")) { } 17 | 18 | public static bool Exists() 19 | { 20 | return !string.IsNullOrEmpty(GlosSIIntegration.GetSettings().DefaultOverlayName); 21 | } 22 | 23 | protected internal override GlosSITargetSettings.LaunchOptions GetPreferredLaunchOptions() 24 | { 25 | // If the same shortcut is used for the Playnite and Default overlay, 26 | // launch options should be the same as the Playnite target, since those actually do something. 27 | // Otherwise, simply launch nothing. That way this default overlay can be launched from Steam 28 | // to be used as simply an overlay, with no particular process associated with it. 29 | if (PlayniteGlosSITarget.Exists()) 30 | { 31 | PlayniteGlosSITarget playniteTarget = new PlayniteGlosSITarget(); 32 | 33 | if (playniteTarget.File.Name == File.Name) 34 | { 35 | return playniteTarget.GetPreferredLaunchOptions(); 36 | } 37 | } 38 | 39 | return new GlosSITargetSettings.LaunchOptions(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Models/Overlays/Types/PlayniteOverlay.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Types; 2 | using GlosSIIntegration.Models.SteamLauncher; 3 | using Playnite.SDK; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace GlosSIIntegration.Models.Overlays.Types 8 | { 9 | class PlayniteOverlay : SteamStartableOverlay 10 | { 11 | private PlayniteOverlay() : base(new PlayniteGlosSITarget()) { } 12 | 13 | /// 14 | /// Creates a Playnite (fullscreen mode) overlay, if one should exist. 15 | /// 16 | /// The overlay if Playnite should have an overlay; null otherwise. 17 | public static PlayniteOverlay Create() 18 | { 19 | if (GlosSIIntegration.Api.ApplicationInfo.Mode == ApplicationMode.Fullscreen 20 | && GlosSIIntegration.GetSettings().UsePlayniteOverlay) 21 | { 22 | try 23 | { 24 | return new PlayniteOverlay(); 25 | } 26 | catch (InvalidOperationException ex) 27 | { 28 | logger.Warn($"Cannot create Playnite overlay: {ex.Message}"); 29 | } 30 | } 31 | 32 | return null; 33 | } 34 | 35 | protected override async Task OnClosedCalled(int overlayExitCode) 36 | { 37 | // We switch to Steam Big Picture mode if the Playnite overlay is closed externally. 38 | // If Steam is already in big picture mode, this simply serves to switch quicker, 39 | // since Steam will take focus once it realizes that overlay has quit. 40 | if (!State.ClosedByExtension) 41 | { 42 | logger.Debug("The Playnite overlay was closed externally: " + 43 | "starting Steam Big Picture mode."); 44 | SteamBigPictureMode.Open(); 45 | } 46 | 47 | await base.OnClosedCalled(overlayExitCode).ConfigureAwait(false); 48 | } 49 | 50 | protected override void OnStartedCalled() { } 51 | 52 | protected override void BeforeClosedCalled() { } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/Models/Overlays/Types/SteamStartableOverlay.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Files; 2 | using GlosSIIntegration.Models.GlosSITargets.Types; 3 | using Playnite.SDK; 4 | using System.Threading.Tasks; 5 | 6 | namespace GlosSIIntegration.Models.Overlays.Types 7 | { 8 | /// 9 | /// Represents an overlay that can be started from Steam such that something is launched, 10 | /// that should otherwise not be launched when the overlay is started from this extension. 11 | /// 12 | internal abstract class SteamStartableOverlay : Overlay 13 | { 14 | protected SteamStartableOverlay(GlosSITarget target) : base(target) { } 15 | 16 | protected override async Task BeforeStartedCalled() 17 | { 18 | await SetDoLaunchGame(false).ConfigureAwait(false); 19 | } 20 | 21 | protected override async Task OnClosedCalled(int overlayExitCode) 22 | { 23 | await SetDoLaunchGame(true).ConfigureAwait(false); 24 | } 25 | 26 | private async Task SetDoLaunchGame(bool doLaunch) 27 | { 28 | LogManager.GetLogger().Trace($"SetDoLaunchGame({doLaunch})"); 29 | GlosSITargetSettings settings = await GlosSITargetSettings.ReadFromAsync(Target.File.FullPath).ConfigureAwait(false); 30 | 31 | if (settings.Launch == null) 32 | { 33 | logger.Error("Launch options of found already running overlay is null!"); 34 | return; 35 | } 36 | 37 | // Check if there is anything to launch. 38 | if (string.IsNullOrEmpty(settings.Launch.LaunchPath)) 39 | { 40 | logger.Trace("LaunchPath of found already running overlay is missing: not updating launch property."); 41 | return; 42 | } 43 | 44 | if (settings.Launch.Launch == doLaunch) 45 | { 46 | logger.Trace("Launch.Launch is already correctly set."); 47 | return; 48 | } 49 | 50 | settings.Launch.Launch = doLaunch; 51 | 52 | await settings.WriteToAsync().ConfigureAwait(false); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crowdin](https://badges.crowdin.net/glossi-integration-playnite/localized.svg)](https://crowdin.com/project/glossi-integration-playnite) 2 | [![Github All Releases](https://img.shields.io/github/downloads/LemmusLemmus/GlosSI-Integration-Playnite/total.svg)](https://playnite.link/addons.html#GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315) 3 | # [GlosSI](https://github.com/Alia5/GlosSI) Integration Extension for [Playnite](https://playnite.link/) 4 | This extension automates creating, removing, launching and closing of GlosSI Steam shortcuts for your games in Playnite. 5 | 6 | ## Why would I want to use this extension? 7 | This extension uses GlosSI. GlosSI lets you use **Steam input** and/or the **Steam overlay** with any game! 8 | - GlosSI runs as a transparent always-on-top window, meaning that GlosSI will work with practically any game. 9 | - Steam input is useful for per-game controller configuration and for using various controllers: Steam supports, among other controllers, PlayStation, Xbox, generic XInput, DirectInput and Steam controllers. 10 | 11 | Apart from all the features that GlosSI offers on its own, this extension makes it easy to use the Steam overlay and Steam input for any game in your Playnite library. Each game can automatically be assigned a separate Steam overlay, allowing for unique controller configurations and making it easier for your Steam friends to see what game you are currently playing. The Steam overlay can be launched automatically when you launch your games. Additionally, when in fullscreen mode a Steam overlay can be assigned to Playnite itself, making it possible to take advantage of Steam input while navigating your Playnite library. 12 | 13 | Note that you can use GlosSI with Playnite without this extension: the extension simply automates some things that may be of interest. 14 | 15 | ## More information 16 | Check out the [wiki](https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/wiki) for information about getting started and general usage of the extension! 17 | 18 | ## Acknowledgements 19 | This extension would not have been possible without JosefNemec and Alia5's amazing work on Playnite and GlosSI respectively! Code from Thomas Pircher and darklinkpower's various extensions was also extremely useful! 20 | 21 | ## Screenshots 22 | Some screenshots of the add-on settings menus, using the default Playnite theme and darklinkpower's Helium theme, fittingly inspired by Steam. 23 | ### Default Theme 24 | Extension settings menu:
25 | ![Screenshot](Screenshots/AddonSettingsDefault.png) 26 |

Overlay creation menu:
27 | ![Screenshot](Screenshots/OverlayShortcutCreationDefault.png) 28 | ### Helium Theme 29 | Extension settings menu:
30 | ![Screenshot](Screenshots/AddonSettingsHelium.png) 31 |

Overlay creation menu:
32 | ![Screenshot](Screenshots/OverlayShortcutCreationHelium.png) 33 | -------------------------------------------------------------------------------- /Source/Views/GlosSIIntegrationSettingsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using Playnite.SDK; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | 7 | namespace GlosSIIntegration 8 | { 9 | public partial class GlosSIIntegrationSettingsView : UserControl 10 | { 11 | public GlosSIIntegrationSettingsView() 12 | { 13 | InitializeComponent(); 14 | UpdateIsEnabled(); 15 | } 16 | 17 | /// 18 | /// Opens the default target .json file for the user to view/edit. 19 | /// 20 | private void EditDefaultGlosSITarget_Click(object sender, RoutedEventArgs e) 21 | { 22 | // TODO: This would be better done via the GlosSI GUI, perphaps by implementing a command line argument. 23 | OpenDefaultGlosSITarget(); 24 | } 25 | 26 | public static void OpenDefaultGlosSITarget() 27 | { 28 | try 29 | { 30 | Process.Start(GlosSIIntegration.GetSettings().DefaultTargetPath); 31 | } 32 | catch (Exception ex) 33 | { 34 | string message = string.Format(ResourceProvider.GetString("LOC_GI_ReadDefaultTargetUnexpectedError"), ex.Message); 35 | LogManager.GetLogger().Error(ex, message); 36 | GlosSIIntegration.Api.Dialogs.ShowErrorMessage(message, ResourceProvider.GetString("LOC_GI_DefaultWindowTitle")); 37 | } 38 | } 39 | 40 | /// 41 | /// Opens a link to the "Tips and Tricks" page on the GitHub wiki. 42 | /// 43 | private void TipsAndTricks_Click(object sender, RoutedEventArgs e) 44 | { 45 | GlosSIIntegrationSettingsViewModel.OpenLink("https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/wiki/Tips-and-tricks"); 46 | } 47 | 48 | /// 49 | /// Opens a link to the "Configuring settings" page on the GitHub wiki. 50 | /// 51 | private void Help_Click(object sender, RoutedEventArgs e) 52 | { 53 | GlosSIIntegrationSettingsViewModel.OpenLink("https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/wiki/Getting-started#configuring-settings"); 54 | } 55 | 56 | /// 57 | /// Updates the IsEnabled property of relevant elements to match the current settings. 58 | /// 59 | private void UpdateIsEnabled(object sender, RoutedEventArgs e) 60 | { 61 | UpdateIsEnabled(); 62 | } 63 | 64 | /// 65 | /// Updates the IsEnabled property of relevant elements to match the current settings. 66 | /// 67 | private void UpdateIsEnabled() 68 | { 69 | // "?? true" should not be reachable. 70 | UsePlayniteOverlayCheckBox.IsEnabled = UseIntegrationFullscreenCheckBox.IsChecked ?? true; 71 | PlayniteOverlayNamePanel.IsEnabled = UsePlayniteOverlayCheckBox.IsEnabled && (UsePlayniteOverlayCheckBox.IsChecked ?? true); 72 | 73 | DefaultOverlayNamePanel.IsEnabled = UseDefaultOverlayCheckBox.IsEnabled && (UseDefaultOverlayCheckBox.IsChecked ?? true); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Shortcuts/GlosSISteamShortcut.cs: -------------------------------------------------------------------------------- 1 | using Playnite.SDK; 2 | using System; 3 | using System.IO; 4 | using GlosSIIntegration.Models.GlosSITargets.Files; 5 | 6 | namespace GlosSIIntegration.Models.GlosSITargets.Shortcuts 7 | { 8 | class GlosSISteamShortcut : SteamShortcut 9 | { 10 | /// 11 | /// Instantiates a GlosSISteamShortcut object belonging to a GlosSITarget shortcut. 12 | /// 13 | /// The name of the shortcut. 14 | /// 15 | /// If the setting is null. 16 | public GlosSISteamShortcut(string name) : base(name, GetGlosSITargetPath()) { } 17 | 18 | // TODO: Calculate in GetSettings() instead, and do the same for GlosSIConfig? 19 | /// 20 | /// Gets the path to the GlosSITarget executable. 21 | /// 22 | /// 23 | /// If the GlosSIPath setting is null. 24 | /// The path to GlosSITarget. 25 | private static string GetGlosSITargetPath() 26 | { 27 | string glosSIFolderPath = GlosSIIntegration.GetSettings().GlosSIPath; 28 | 29 | if (glosSIFolderPath == null) 30 | { 31 | throw new InvalidOperationException("The path to GlosSI has not been set."); 32 | } 33 | 34 | return Path.Combine(glosSIFolderPath, "GlosSITarget.exe").Replace('\\', '/'); 35 | } 36 | 37 | /// 38 | /// Runs the GlosSITarget associated with this object via Steam. 39 | /// If the GlosSI configuration file could not be found, 40 | /// the method only displays an error notification. 41 | /// 42 | /// If the GlosSITarget process is not runnable 43 | /// (i.e. returns false) or if starting the process failed. 44 | /// 45 | public override void Run() 46 | { 47 | LogManager.GetLogger().Debug($"Running GlosSITarget for {Name}..."); 48 | VerifyRunnable(); 49 | base.Run(); 50 | } 51 | 52 | // TODO: Below only checks if the target file exists, not whether the shortcut has actually been added to Steam. 53 | /// 54 | /// Verifies that the GlosSITarget shortcut is runnable. If not, throws an exception and displays an error message. 55 | /// 56 | /// If the shortcut is not runnable (i.e. does not have a .json file). 57 | public void VerifyRunnable() 58 | { 59 | if (!new GlosSITargetFileInfo(Name).Exists()) 60 | { 61 | string msg = ResourceProvider.GetString("LOC_GI_GlosSITargetNotFoundOnGameStartError"); 62 | GlosSIIntegration.NotifyError(msg, "GlosSIIntegration-SteamGame-RunGlosSITarget"); 63 | throw new InvalidOperationException(msg); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Source/Models/SteamLauncher/SteamBigPictureMode.cs: -------------------------------------------------------------------------------- 1 | using Playnite.SDK; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | 6 | namespace GlosSIIntegration.Models.SteamLauncher 7 | { 8 | internal class SteamBigPictureMode : ISteamMode 9 | { 10 | private static readonly ILogger logger = LogManager.GetLogger(); 11 | public WinWindow MainWindow { get; } 12 | 13 | internal SteamBigPictureMode(WinWindow mainWindow) 14 | { 15 | MainWindow = mainWindow; 16 | } 17 | 18 | /// 19 | /// Exits Big Picture mode. Steam desktop mode will eventually start. 20 | /// 21 | public void Exit() 22 | { 23 | try 24 | { 25 | MainWindow.Close(); 26 | } 27 | catch (InvalidOperationException ex) 28 | { 29 | logger.Warn(ex, "Could not close Steam Big Picture mode."); 30 | } 31 | } 32 | 33 | /// 34 | /// Opens Steam Big Picture mode. If Big Picture Mode is not already running, starts it. 35 | /// 36 | public static void Open() 37 | { 38 | Process.Start("steam://open/bigpicture")?.Dispose(); 39 | } 40 | 41 | /// 42 | /// Prevents focus theft from occuring by exiting Big Picture mode. 43 | /// This must be called a Steam shortcut that should not steal focus is closed. 44 | /// 45 | public async Task PreventFocusTheft() 46 | { 47 | // When Steam is in big picture mode, whenever a game is closed Steam will take focus (after roughly 3 seconds). 48 | // This is annoying and I cannot be bothered to deal with all particularities of when/how Steam Big Picture mode steals focus. 49 | // As such, we simply return Steam to desktop mode. 50 | // Steam desktop mode should provide mostly the same experience provided that the user has not disabled 51 | // "Use the Big Picture Overlay when using a controller". 52 | 53 | try 54 | { 55 | await SteamDesktopMode.StealthilyReturnSteamToDesktopMode(this).ConfigureAwait(false); 56 | } 57 | catch (TimeoutException ex) 58 | { 59 | logger.Error(ex, "Failed to switch to Steam desktop mode. " + 60 | "The extension switches to Steam desktop mode in order to avoid having to deal with " + 61 | "Steam Big Picture mode force focusing itself when games are closed."); 62 | } 63 | 64 | // Proper handling of Steam Big Picture mode taking focus could be added later. 65 | // For example, Steam BPM does not take focus if there are any currently running games. 66 | // As such, merely switching overlays is fine. 67 | // A "dummy" Steam shortcut could also work. 68 | // Alternatively, the most effective but also most intrusive option would be to 69 | // hook Steam's RaiseWindow() (SDL) or AttachThreadInput() (Win32) calls. 70 | 71 | // Note that when a new Steam game is launched while in BPM, the current Steam overlay will display the game starting. 72 | // Hiding GlosSITarget before the overlay is switched solves that problem. 73 | 74 | // It is also worth noting that when Steam is in BPM, Steam will play a sound when a game is started. 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Source/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace GlosSIIntegration.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GlosSIIntegration.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Bitmap. 65 | /// 66 | internal static System.Drawing.Bitmap DefaultSteamShortcutIcon { 67 | get { 68 | object obj = ResourceManager.GetObject("DefaultSteamShortcutIcon", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Byte[]. 75 | /// 76 | internal static byte[] DefaultTarget { 77 | get { 78 | object obj = ResourceManager.GetObject("DefaultTarget", resourceCulture); 79 | return ((byte[])(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Files/GameGlosSITargetFile.cs: -------------------------------------------------------------------------------- 1 | using GlosSIIntegration.Models.GlosSITargets.Types; 2 | using System.IO; 3 | 4 | namespace GlosSIIntegration.Models.GlosSITargets.Files 5 | { 6 | internal class GameGlosSITargetFile : GlosSITargetFile 7 | { 8 | private readonly GameGlosSITarget target; 9 | private PlayniteGameSteamAssets SteamAssets => new PlayniteGameSteamAssets(target.AssociatedGame, target); 10 | 11 | public GameGlosSITargetFile(GameGlosSITarget target) : base(target) 12 | { 13 | this.target = target; 14 | } 15 | 16 | private string GetPathToGameIcon() 17 | { 18 | if (string.IsNullOrEmpty(target.AssociatedGame.Icon)) return null; 19 | 20 | return Path.Combine(GlosSIIntegration.Api.Paths.ConfigurationPath, @"library\files\", target.AssociatedGame.Icon); 21 | } 22 | 23 | /// 24 | /// Creates a GlosSITarget and Steam shortcut for a game, using the default .json structure. 25 | /// Already integrated games and games tagged for ignoring are ignored. 26 | /// 27 | /// A path to the icon of the shortcut. The path can be null for no icon. 28 | /// true if the GlosSITarget was created; false if creation was ignored. 29 | /// If the default target json-file could not be found. 30 | /// If the glosSITargetsPath directory could not be found. 31 | /// 32 | public override bool Create(string iconPath) 33 | { 34 | if (!GlosSIIntegration.GameHasIgnoredTag(target.AssociatedGame) && 35 | !GlosSIIntegration.GameHasIntegratedTag(target.AssociatedGame) && 36 | base.Create(iconPath)) 37 | { 38 | GlosSIIntegration.AddTagToGame(GlosSIIntegration.LOC_INTEGRATED_TAG, target.AssociatedGame); 39 | SteamAssets.SetFromPlayniteAssets(false); 40 | return true; 41 | } 42 | 43 | return false; 44 | } 45 | 46 | /// 47 | /// Creates a GlosSITarget and Steam shortcut for a game, using the default .json structure. 48 | /// Already integrated games and games tagged for ignoring are ignored. 49 | /// Tries to use the same icon as the Playnite game. 50 | /// 51 | /// true if the GlosSITarget was created; false if creation was ignored. 52 | /// If the default target json-file could not be found. 53 | /// If the glosSITargetsPath directory could not be found. 54 | /// 55 | /// 56 | public override bool Create() 57 | { 58 | return Create(GetPathToGameIcon()); 59 | } 60 | 61 | public override void Overwrite() 62 | { 63 | base.Overwrite(); 64 | SteamAssets.SetFromPlayniteAssets(false); 65 | } 66 | 67 | public override bool Remove() 68 | { 69 | if (GlosSIIntegration.GameHasIntegratedTag(target.AssociatedGame)) 70 | { 71 | GlosSIIntegration.RemoveTagFromGame(GlosSIIntegration.LOC_INTEGRATED_TAG, target.AssociatedGame); 72 | GlosSIIntegration.RemoveTagFromGame(GlosSIIntegration.SRC_INTEGRATED_TAG, target.AssociatedGame); 73 | SteamAssets.DeleteAllAssets(); 74 | return base.Remove(); 75 | } 76 | 77 | return false; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Source/Models/GlosSITargets/Shortcuts/SteamShortcut.cs: -------------------------------------------------------------------------------- 1 | using Playnite.SDK; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace GlosSIIntegration.Models.GlosSITargets.Shortcuts 8 | { 9 | /// 10 | /// Represents a shortcut to a Steam game. 11 | /// 12 | internal class SteamShortcut 13 | { 14 | private readonly Lazy id; 15 | /// 16 | /// The appID of the Steam shortcut. 17 | /// 18 | public ulong Id => id.Value; 19 | 20 | /// 21 | /// The name of the Steam shortcut. 22 | /// 23 | public string Name { get; } 24 | 25 | /// 26 | /// Constructor for a non-Steam game shortcut. 27 | /// 28 | /// The name of the game. 29 | /// The path to the game executable. 30 | public SteamShortcut(string name, string path) 31 | { 32 | Name = name; 33 | id = new Lazy(() => 34 | { 35 | Crc algorithm = new Crc(32, 0x04C11DB7, true, 0xffffffff, true, 0xffffffff); 36 | string input = UTF8ToCodeUnits("\"" + path + "\"" + Name); 37 | uint top32 = algorithm.BitByBit(input) | 0x80000000; 38 | return (((ulong)top32) << 32) | 0x02000000; 39 | }); 40 | } 41 | 42 | // TODO: Reverse this process and use the information to: 43 | // A) Search the .vdf file to verify that shortcuts have been added. 44 | // B) Display the correct Steam user name when there are multiple users to pick from 45 | // (when getting the path to shortcuts.vdf). 46 | private static string UTF8ToCodeUnits(string str) 47 | { 48 | return new string(Encoding.UTF8.GetBytes(str).Select(b => (char)b).ToArray()); 49 | } 50 | 51 | /// 52 | /// Runs the Steam shortcut. 53 | /// 54 | /// If starting the process failed. 55 | public virtual void Run() 56 | { 57 | LogManager.GetLogger().Info($"Starting Steam game \"{this}\"."); 58 | 59 | try 60 | { 61 | // The command "steam://rungameid/" was used before, 62 | // since the below command apparently did not work with non-Steam shortcuts before. 63 | // The command has been changed because "steam://rungameid/" shows 64 | // a "Launching..." pop-up window, which is undesirable when starting GlosSITarget. 65 | // Another (in this case irrelevant) difference is that "/Dialog" can be appended 66 | // to the command below to show multiple launch options (if there are any). 67 | // Other differences are unknown. 68 | Process.Start("steam://launch/" + Id.ToString())?.Dispose(); 69 | } 70 | catch (Exception ex) when (ex is System.ComponentModel.Win32Exception 71 | || ex is ObjectDisposedException 72 | || ex is System.IO.FileNotFoundException) 73 | { 74 | string msg = string.Format( 75 | ResourceProvider.GetString("LOC_GI_RunSteamGameUnexpectedError"), ex.Message); 76 | GlosSIIntegration.NotifyError(msg, "GlosSIIntegration-SteamGame-Run"); 77 | throw new InvalidOperationException(msg, ex); 78 | } 79 | } 80 | 81 | public override bool Equals(object obj) 82 | { 83 | // TODO: Compare name (and path) instead? 84 | return obj is SteamShortcut other && Id == other.Id; 85 | } 86 | 87 | public override int GetHashCode() 88 | { 89 | return (int)Id; 90 | } 91 | 92 | public override string ToString() 93 | { 94 | return $"{Name}: {Id}"; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Source/Models/Overlays/Types/ExternallyStartedOverlay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using GlosSIIntegration.Models.GlosSITargets.Files; 5 | using GlosSIIntegration.Models.GlosSITargets.Types; 6 | using GlosSIIntegration.Models.SteamLauncher; 7 | 8 | namespace GlosSIIntegration.Models.Overlays.Types 9 | { 10 | /// 11 | /// Represents an unidentified overlay, started from outside this extension. 12 | /// 13 | internal class ExternallyStartedOverlay : Overlay, IDisposable 14 | { 15 | private ExternallyStartedOverlay(string name, Process process) : base(new UnidentifiedGlosSITarget(name)) 16 | { 17 | StartedExternally(process); 18 | } 19 | 20 | /// 21 | /// Does nothing if overlay has been replaced or closed, otherwise disposes the process. 22 | /// 23 | public void Dispose() 24 | { 25 | State.GlosSITargetProcess?.Dispose(); 26 | } 27 | 28 | public Task WaitForExit() 29 | { 30 | return State.GlosSITargetProcess.WaitForExitAsyncSafe(); 31 | } 32 | 33 | public static async Task GetCurrent() 34 | { 35 | return await GetExternallyStartedOverlay(GlosSITargetProcess.GetRunning()).ConfigureAwait(false); 36 | } 37 | 38 | /// 39 | /// Gets the externally started overlay from the GlosSITarget process . 40 | /// Passing means giving away the responsibility 41 | /// of disposing the process to the overlay. 42 | /// 43 | /// 44 | /// The externally started overlay, or null if no overlay could be found or 45 | /// if the is null. 46 | public static async Task GetExternallyStartedOverlay(Process alreadyRunningProcess) 47 | { 48 | if (alreadyRunningProcess == null) return null; 49 | 50 | GlosSITargetSettings currentSettings; 51 | 52 | try 53 | { 54 | currentSettings = await GlosSITargetSettings.ReadCurrent().ConfigureAwait(false); 55 | } 56 | catch (System.Net.Http.HttpRequestException ex) 57 | { 58 | // Should not happen since a process was found (unless the process happened to close inbetween). 59 | logger.Error(ex, "Failed to read currently running GlosSITarget settings."); 60 | alreadyRunningProcess.Dispose(); 61 | return null; 62 | } 63 | 64 | string overlayName = currentSettings.Name; 65 | 66 | if (string.IsNullOrEmpty(overlayName)) 67 | { 68 | logger.Error("The name of the currently running overlay is missing."); 69 | alreadyRunningProcess.Dispose(); 70 | return null; 71 | } 72 | 73 | // Hopefully a temporary solution to focus loss from Steam BPM detecting games as having closed. 74 | if (StartFromSteamLaunchOptions.LaunchesPlaynite(currentSettings.Launch) && 75 | Steam.Mode is SteamBigPictureMode bpmMode) 76 | { 77 | logger.Debug("Preventing eventual focus loss by switching to Steam desktop mode."); 78 | await bpmMode.PreventFocusTheft().ConfigureAwait(false); 79 | } 80 | 81 | return new ExternallyStartedOverlay(overlayName, alreadyRunningProcess); 82 | } 83 | 84 | protected override Task BeforeStartedCalled() 85 | { 86 | return Task.CompletedTask; 87 | } 88 | 89 | protected override void OnStartedCalled() { } 90 | 91 | protected override void BeforeClosedCalled() { } 92 | 93 | protected override Task OnClosedCalled(int overlayExitCode) 94 | { 95 | return Task.CompletedTask; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Source/Views/ShortcutCreationView.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 |