├── .gitattributes
├── .gitignore
├── InstallerManifest.yaml
├── LICENSE
├── README.md
├── Screenshots
├── AddonSettingsDefault.png
├── AddonSettingsHelium.png
├── OverlayShortcutCreationDefault.png
├── OverlayShortcutCreationHelium.png
└── Thumbnails
│ └── AddonSettingsDefault.jpg
├── Source
├── App.xaml
├── GlosSIIntegration.cs
├── GlosSIIntegration.csproj
├── GlosSIIntegration.sln
├── GlosSIIntegrationSettings.cs
├── Localization
│ ├── ar_SA.xaml
│ ├── de_DE.xaml
│ ├── en_US.xaml
│ ├── es_ES.xaml
│ ├── fr_FR.xaml
│ ├── it_IT.xaml
│ ├── loc_source.xaml
│ ├── no_NO.xaml
│ ├── pl_PL.xaml
│ ├── pt_BR.xaml
│ ├── ro_RO.xaml
│ ├── ru_RU.xaml
│ ├── uk_UA.xaml
│ └── zh_CN.xaml
├── Models
│ ├── GlosSITargets
│ │ ├── Files
│ │ │ ├── GameGlosSITargetFile.cs
│ │ │ ├── GlosSITargetFile.cs
│ │ │ ├── GlosSITargetFileInfo.cs
│ │ │ ├── GlosSITargetSettings.cs
│ │ │ ├── JsonExtensions.cs
│ │ │ └── StartFromSteamLaunchOptions.cs
│ │ ├── KnownTargets.cs
│ │ ├── Shortcuts
│ │ │ ├── Crc.cs
│ │ │ ├── GlosSISteamShortcut.cs
│ │ │ └── SteamShortcut.cs
│ │ ├── TargetsVersionMigrator.cs
│ │ └── Types
│ │ │ ├── DefaultGlosSITarget.cs
│ │ │ ├── GameGlosSITarget.cs
│ │ │ ├── GlosSITarget.cs
│ │ │ ├── PlayniteGlosSITarget.cs
│ │ │ └── UnidentifiedGlosSITarget.cs
│ ├── HardLink.cs
│ ├── Overlays
│ │ ├── GlosSITargetProcess.cs
│ │ ├── OverlaySwitchingCoordinator.cs
│ │ ├── OverlaySwitchingDecisionMaker.cs
│ │ └── Types
│ │ │ ├── DefaultGameOverlay.cs
│ │ │ ├── ExternallyStartedOverlay.cs
│ │ │ ├── GameOverlay.cs
│ │ │ ├── Overlay.cs
│ │ │ ├── PlayniteOverlay.cs
│ │ │ └── SteamStartableOverlay.cs
│ ├── PlayniteGameSteamAssets.cs
│ ├── ProcessExtensions.cs
│ ├── SteamGameAssets.cs
│ ├── SteamLauncher
│ │ ├── ISteamMode.cs
│ │ ├── Steam.cs
│ │ ├── SteamBigPictureMode.cs
│ │ └── SteamDesktopMode.cs
│ └── WinWindow.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ └── Resources.resx
├── Resources
│ ├── DefaultSteamShortcutIcon.png
│ └── DefaultTarget.json
├── Scripts
│ └── StartPlayniteFromGlosSI.vbs
├── ViewModels
│ ├── GlosSIIntegrationSettingsViewModel.cs
│ └── ShortcutCreationViewModel.cs
├── Views
│ ├── GlosSIIntegrationSettingsView.xaml
│ ├── GlosSIIntegrationSettingsView.xaml.cs
│ ├── ShortcutCreationView.xaml
│ └── ShortcutCreationView.xaml.cs
├── extension.yaml
├── icon.png
└── packages.config
└── crowdin.yml
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/InstallerManifest.yaml:
--------------------------------------------------------------------------------
1 | AddonId: GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315
2 | Packages:
3 | - Version: 1.2.2
4 | RequiredApiVersion: 6.9.0
5 | ReleaseDate: 2024-11-09
6 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.2.2/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_2_2.pext
7 | Changelog:
8 | - Fixed an error when adding a Playnite game with an image whose file extension is not supported by Steam.
9 | - Version: 1.2.1
10 | RequiredApiVersion: 6.9.0
11 | ReleaseDate: 2024-07-11
12 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.2.1/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_2_1.pext
13 | Changelog:
14 | - Fixed an error when a Playnite game does not have a cover or background image.
15 | - Version: 1.2.0
16 | RequiredApiVersion: 6.9.0
17 | ReleaseDate: 2024-06-23
18 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.2.0/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_2_0.pext
19 | Changelog:
20 | - Shortcut switching has been redesigned. It should hopefully be more reliable now, and window focus should no longer be lost at times.
21 | - The "Launching..." popup that Steam shows when starting a non-Steam shortcut is now gone! This also appears to speed up launching of shortcuts.
22 | - The shortcuts created by this extension can now be launched from the Steam library!
23 | - Launching the "Playnite Overlay" from Steam will now start Playnite, and closing it will start Steam BPM.
24 | - Playnite game images will now be carried over to the non-Steam shortcut in the Steam library, if possible.
25 | - The default GlosSITarget settings have been updated, for example to not show the GlosSI logging.
26 | - Various bug fixes.
27 | - Added Spanish, Chinese (Simplified) and French translations. Thanks, Madcore, BennyDung and enzomtp!
28 | - This update has been long-overdue, most of these changes were implemented a year ago. I had simply not released them yet, due to fear of introducing new bugs. If you encounter any, please report them!
29 | - Since maintenance of GlosSI has stopped for the time being, the future of this extension is uncertain...
30 | - Version: 1.1.2
31 | RequiredApiVersion: 6.5.0
32 | ReleaseDate: 2022-11-16
33 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.1.2/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_1_2.pext
34 | Changelog:
35 | - Added more Spanish translations. Thanks, darklinkpower!
36 | - Changed the default settings to enable the integration by default.
37 | - Fixed a rare crash when the path to GlosSI has not been set and a game is launched.
38 | - Fixed the GlosSI path text box not updating visually.
39 | - Version: 1.1.1
40 | RequiredApiVersion: 6.5.0
41 | ReleaseDate: 2022-11-09
42 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.1.1/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_1_1.pext
43 | Changelog:
44 | - Fixed extension crashing when closing certain games.
45 | - Fixed starting of game overlays if they are started before the Playnite overlay finishes starting.
46 | - Minor timeout and logging changes.
47 | - Version: 1.1.0
48 | RequiredApiVersion: 6.5.0
49 | ReleaseDate: 2022-11-04
50 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.1.0/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_1_0.pext
51 | Changelog:
52 | - Added option to close the game when the overlay is closed.
53 | - Added more Spanish translations. Thanks, Madcore!
54 | - Added some semblance of support for running multiple games simultaneously.
55 | - Added a link to tips and tricks in the settings menu.
56 | - Improved game startup times slightly.
57 | - The Playnite overlay now starts slightly earlier when launching Playnite Fullscreen mode.
58 | - Updated the extension icon.
59 | - Fixed overlays with non-ASCII characters not launching.
60 | - Fixed overlay closing and opening unnecessarily when the same Steam shortcut is used for two overlays (i.e. the Playnite overlay and the default overlay).
61 | - Various minor changes.
62 | - Added bugs (probably). If you find any, please report them on GitHub or Discord!
63 | - Version: 1.0.5
64 | RequiredApiVersion: 6.2.2
65 | ReleaseDate: 2022-10-07
66 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.0.5/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_0_5.pext
67 | Changelog:
68 | - Fixed Playnite losing focus in fullscreen mode when using the Playnite overlay.
69 | - Fixed the extension being unable to close GlosSITargets with a disabled Steam/GlosSI overlay.
70 | - Demoted the "failed to close the Steam overlay" error pop-up to a notification.
71 | - Version: 1.0.4
72 | RequiredApiVersion: 6.2.2
73 | ReleaseDate: 2022-09-28
74 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.0.4/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_0_4.pext
75 | Changelog:
76 | - Fixed the default Steam overlay incorrectly starting for Steam games.
77 | - Fixed an error message.
78 | - Version: 1.0.3
79 | RequiredApiVersion: 6.2.2
80 | ReleaseDate: 2022-09-23
81 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.0.3/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_0_3.pext
82 | Changelog:
83 | - Added Italian translations. Thanks StarFang208!
84 | - Fixed the appearance of a hyperlink.
85 | - Version: 1.0.2
86 | RequiredApiVersion: 6.2.2
87 | ReleaseDate: 2022-08-16
88 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.0.2/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_0_2.pext
89 | Changelog:
90 | - Fixed a bug where games with no icon could not be added or removed.
91 | - Version: 1.0.1
92 | RequiredApiVersion: 6.2.2
93 | ReleaseDate: 2022-08-11
94 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.0.1/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_0_1.pext
95 | Changelog:
96 | - Added Spanish translations. Thanks darklinkpower!
97 | - Minor localization changes.
98 | - Version: 1.0.0
99 | RequiredApiVersion: 6.2.2
100 | ReleaseDate: 2022-08-10
101 | PackageUrl: https://github.com/LemmusLemmus/GlosSI-Integration-Playnite/releases/download/v1.0.0/GlosSIIntegration_6b0297da-75e5-4330-bb2d-b64bff22c315_1_0_0.pext
102 | Changelog:
103 | - Initial release
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://crowdin.com/project/glossi-integration-playnite)
2 | [](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 | 
26 |
Overlay creation menu:
27 | 
28 | ### Helium Theme
29 | Extension settings menu:
30 | 
31 |
Overlay creation menu:
32 | 
33 |
--------------------------------------------------------------------------------
/Screenshots/AddonSettingsDefault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Screenshots/AddonSettingsDefault.png
--------------------------------------------------------------------------------
/Screenshots/AddonSettingsHelium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Screenshots/AddonSettingsHelium.png
--------------------------------------------------------------------------------
/Screenshots/OverlayShortcutCreationDefault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Screenshots/OverlayShortcutCreationDefault.png
--------------------------------------------------------------------------------
/Screenshots/OverlayShortcutCreationHelium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Screenshots/OverlayShortcutCreationHelium.png
--------------------------------------------------------------------------------
/Screenshots/Thumbnails/AddonSettingsDefault.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Screenshots/Thumbnails/AddonSettingsDefault.jpg
--------------------------------------------------------------------------------
/Source/App.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Source/GlosSIIntegration.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F}
8 | Library
9 | Properties
10 | GlosSIIntegration
11 | GlosSIIntegration
12 | v4.6.2
13 | 512
14 | true
15 |
16 |
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | pdbonly
27 | true
28 | bin\Release\
29 | TRACE
30 | prompt
31 | 4
32 |
33 |
34 |
35 | packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
36 |
37 |
38 | packages\PlayniteSDK.6.9.0\lib\net462\Playnite.SDK.dll
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | True
88 | True
89 | Resources.resx
90 |
91 |
92 |
93 | ShortcutCreationView.xaml
94 |
95 |
96 |
97 |
98 |
99 | GlosSIIntegrationSettingsView.xaml
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | PreserveNewest
110 |
111 |
112 |
113 |
114 |
115 | MSBuild:Compile
116 | Designer
117 |
118 |
119 | Designer
120 | MSBuild:Compile
121 | PreserveNewest
122 |
123 |
124 | MSBuild:Compile
125 | Designer
126 | PreserveNewest
127 |
128 |
129 | MSBuild:Compile
130 | Designer
131 | PreserveNewest
132 |
133 |
134 | MSBuild:Compile
135 | Designer
136 | PreserveNewest
137 |
138 |
139 | MSBuild:Compile
140 | Designer
141 | PreserveNewest
142 |
143 |
144 | MSBuild:Compile
145 | Designer
146 | PreserveNewest
147 |
148 |
149 | MSBuild:Compile
150 | Designer
151 | PreserveNewest
152 |
153 |
154 | MSBuild:Compile
155 | Designer
156 | PreserveNewest
157 |
158 |
159 | MSBuild:Compile
160 | Designer
161 | PreserveNewest
162 |
163 |
164 | MSBuild:Compile
165 | Designer
166 | PreserveNewest
167 |
168 |
169 | MSBuild:Compile
170 | Designer
171 | PreserveNewest
172 |
173 |
174 | MSBuild:Compile
175 | Designer
176 | PreserveNewest
177 |
178 |
179 | MSBuild:Compile
180 | Designer
181 | PreserveNewest
182 |
183 |
184 | MSBuild:Compile
185 | Designer
186 |
187 |
188 | Designer
189 | MSBuild:Compile
190 |
191 |
192 | Designer
193 | MSBuild:Compile
194 |
195 |
196 |
197 |
198 | PreserveNewest
199 |
200 |
201 |
202 |
203 | ResXFileCodeGenerator
204 | Designer
205 | Resources.Designer.cs
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 | PreserveNewest
214 |
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/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/GlosSIIntegrationSettings.cs:
--------------------------------------------------------------------------------
1 | using Playnite.SDK;
2 | using Playnite.SDK.Data;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Linq;
8 |
9 | namespace GlosSIIntegration
10 | {
11 | public class GlosSIIntegrationSettings : ObservableObject
12 | {
13 | // TODO: Consider using System.Uri instead of string.
14 |
15 | private bool closeGameWhenOverlayIsClosed = true;
16 | private string glosSIPath = null;
17 | private readonly string glosSITargetsPath = Environment.ExpandEnvironmentVariables(@"%appdata%\GlosSI\Targets");
18 | private string steamShortcutsPath = null;
19 | private readonly string defaultTargetPath = Path.Combine(GlosSIIntegration.Instance.GetPluginUserDataPath(), "DefaultTarget.json");
20 | private readonly string knownTargetsPath = Path.Combine(GlosSIIntegration.Instance.GetPluginUserDataPath(), "Targets.json");
21 | private readonly string startPlayniteFromGlosSIScriptPath = Path.Combine(Path.GetDirectoryName(typeof(GlosSIIntegrationSettings).Assembly.Location),
22 | @"Scripts\StartPlayniteFromGlosSI.vbs");
23 | private string playniteOverlayName = null;
24 | private bool usePlayniteOverlay = false;
25 | private bool useIntegrationFullscreen = true;
26 | private bool defaultUseIntegrationDesktop = true;
27 | private bool useDefaultOverlay = false;
28 | private string defaultOverlayName = null;
29 | private Version glosSIVersion = null;
30 |
31 | public bool CloseGameWhenOverlayIsClosed { get => closeGameWhenOverlayIsClosed; set => SetValue(ref closeGameWhenOverlayIsClosed, value); }
32 | // TODO: Rename to GlosSIFolderPath or GlosSIDirectory?
33 | ///
34 | /// The path to the GlosSI folder containing GlosSIConfig.exe and GlosSITarget.exe.
35 | /// Setting GlosSIPath also updates the current GlosSIVersion.
36 | ///
37 | public string GlosSIPath { get => glosSIPath; set { if (value != glosSIPath) { glosSIVersion = null; } SetValue(ref glosSIPath, value); } }
38 | // TODO: Instead of relying on the path to shortcuts.vdf, only save the Steam user ID and use it
39 | // (as well as the Steam path registry value) to calculate all needed Steam paths.
40 | // Set the user ID only once, and do not permit changing it via the UI (unless the user has been informed of the consequences).
41 | // Could write a short wiki page detailing the process of changing the user ID.
42 | ///
43 | /// The path to the shortcuts.vdf file, containing all Steam non-Steam shortcuts.
44 | /// Note that the file does not necessarily exist, if no shortcut has ever been added.
45 | ///
46 | public string SteamShortcutsPath { get => steamShortcutsPath; set => SetValue(ref steamShortcutsPath, value); }
47 | public string PlayniteOverlayName { get => playniteOverlayName; set => SetValue(ref playniteOverlayName, value); }
48 | public bool UsePlayniteOverlay { get => usePlayniteOverlay; set => SetValue(ref usePlayniteOverlay, value); }
49 | public bool UseIntegrationFullscreen { get => useIntegrationFullscreen; set => SetValue(ref useIntegrationFullscreen, value); }
50 | public bool DefaultUseIntegrationDesktop { get => defaultUseIntegrationDesktop; set => SetValue(ref defaultUseIntegrationDesktop, value); }
51 | public bool UseDefaultOverlay { get => useDefaultOverlay; set => SetValue(ref useDefaultOverlay, value); }
52 | public string DefaultOverlayName { get => defaultOverlayName; set => SetValue(ref defaultOverlayName, value); }
53 |
54 | ///
55 | /// Read-only and therefore thread-safe.
56 | ///
57 | [DontSerialize]
58 | public string GlosSITargetsPath { get => glosSITargetsPath; }
59 | [DontSerialize]
60 | public string StartPlayniteFromGlosSIScriptPath { get => startPlayniteFromGlosSIScriptPath; }
61 | ///
62 | /// If the the file does not exist, creates it.
63 | ///
64 | [DontSerialize]
65 | public string DefaultTargetPath { get => GetDefaultTargetPath(); }
66 | [DontSerialize]
67 | public string KnownTargetsPath { get => knownTargetsPath; }
68 | [DontSerialize]
69 | public Version GlosSIVersion { get => GetGlosSIVersion(); set => glosSIVersion = value; }
70 |
71 | private Version GetGlosSIVersion()
72 | {
73 | if (glosSIVersion == null && GlosSIPath != null)
74 | {
75 | try
76 | {
77 | string glosSIConfigPath = Path.Combine(GlosSIPath, "GlosSIConfig.exe");
78 | string version = FileVersionInfo.GetVersionInfo(glosSIConfigPath).ProductVersion;
79 | version = string.Concat(version.TakeWhile(c => char.IsDigit(c) || c == '.'));
80 | glosSIVersion = new Version(version);
81 | }
82 | catch (Exception e)
83 | {
84 | RethrowException(e, "LOC_GI_GetGlosSIVersionUnexpectedError");
85 | }
86 | }
87 |
88 | return glosSIVersion;
89 | }
90 |
91 | private string GetDefaultTargetPath()
92 | {
93 | if (!File.Exists(defaultTargetPath))
94 | {
95 | CreateDefaultTarget();
96 | }
97 |
98 | return defaultTargetPath;
99 | }
100 |
101 | ///
102 | /// Creates the "DefaultTargets.json" file. If the file already exists, overwrites it.
103 | ///
104 | public void CreateDefaultTarget()
105 | {
106 | try
107 | {
108 | LogManager.GetLogger().Trace("Creating DefaultTarget file...");
109 | File.WriteAllBytes(defaultTargetPath, Properties.Resources.DefaultTarget);
110 | LogManager.GetLogger().Info("DefaultTarget file created.");
111 | }
112 | catch (Exception e)
113 | {
114 | RethrowException(e, "LOC_GI_CreateDefaultTargetFileUnexpectedError"); // TODO: Unnecessary localized string?
115 | }
116 | }
117 |
118 | private void RethrowException(Exception e, string locKey)
119 | {
120 | string message = string.Format(locKey, e.Message);
121 | LogManager.GetLogger().Error(message);
122 | throw new Exception(message, e);
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/Source/Localization/loc_source.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | GlosSI Integration
6 | Steam must be restarted for the changes to take effect.
7 | GlosSI Integration failed to run the Steam shortcut: The GlosSI .json target file could not be found.
8 | Disable GlosSI Integration
9 | Enable GlosSI Integration
10 |
11 |
12 | Backing up GlosSI configuration files...
13 | Backing up Steam shortcuts file...
14 | Adding GlosSI integration to games...
15 | Removing GlosSI integration from games...
16 |
17 |
18 | The selected game "{0}" is a Steam game and should already support the Steam overlay/input. Are you sure you want to add the game?
19 | All of the selected games are Steam games and should already support the Steam overlay/input. Are you sure you want to add the games?
20 | No games were added as GlosSI Steam shortcuts. This could be due to the games already having been added or having the ignored tag.
21 | One game was successfully added as GlosSI Steam shortcut{0}. {1}
22 | {0} games were successfully added as GlosSI Steam shortcuts{1}. {2}
23 | (one game was skipped)
24 | ({0} games were skipped)
25 |
26 |
27 | No GlosSI/Steam integrations were removed.
28 | The GlosSI/Steam integration of one game was removed.
29 | The GlosSI/Steam integration of {0} games were removed.
30 |
31 |
32 | [GI] Integrated
33 | [GI] Ignored
34 |
35 |
36 | GlosSI Integration
37 | Add Integration
38 | Remove Integration
39 | At least one game name and/or icon path contains non-ASCII characters. These games will be skipped as the installed version of GlosSI does not reliably support all Unicode characters.
40 |
41 |
42 | General Settings
43 | Desktop Mode Settings
44 | Fullscreen Mode Settings
45 | Path Settings (required)
46 | Edit the GlosSI configuration used when creating new shortcuts.
47 | Tips and tricks!
48 | When the game overlay is closed, also close the game
49 | Use a Default Steam overlay
50 | Use a Playnite Steam overlay
51 | Default overlay name:
52 | Playnite overlay name:
53 | Add…
54 | Enable GlosSI Integration by default in desktop mode
55 | Use GlosSI Integration in fullscreen mode
56 | Steam shortcuts path:
57 | GlosSI folder path:
58 | Which of these Steam accounts should the GlosSI integration add shortcuts to? The plugin needs the path to shortcuts.vdf.
59 | User with Friend Code: "{0}"
60 | Steam shortcuts file
61 | The installed GlosSI version is an older one and the extension is not guaranteed to work with it. Please consider updating GlosSI for the latest features and bug fixes.
62 |
63 |
64 | Create a new Steam shortcut
65 | Steam shortcut name:
66 | Steam shortcut icon:
67 | Image
68 | The name and/or icon path contains non-ASCII characters. The installed version of GlosSI does not reliably support all Unicode characters.
69 |
70 |
71 | Required settings are missing/incorrect. Go to the settings menu?
72 | Required settings are missing/incorrect. Please visit the settings menu in desktop mode.
73 | Required settings are missing/incorrect.
74 | The GlosSI Targets folder could not be found. GlosSI must be installed and have run at least once before the GlosSI Integration extension can be used.
75 |
76 |
77 | The path to shortcuts.vdf has not been set.
78 | The path to the GlosSI folder has not been set.
79 | The shortcuts.vdf path does not lead to a file called "shortcuts.vdf".
80 | The shortcuts.vdf file could not be found.
81 | The GlosSI folder location could not be found.
82 | The GlosSI folder location is incorrect: the GlosSI executables could not be found.
83 | The shortcuts.vdf file location is incorrect: the file should be located inside the "config" folder in the Steam installation folder.
84 |
85 |
86 | Playnite
87 | default
88 | The name of the {0} overlay has not been set.
89 | The target file referenced by the {0} overlay name could not be found.
90 | Something is wrong with the file referenced by the {0} overlay name. The name property in the target .json file in %appdata%\GlosSI\Targets could not be found or has not been set.
91 |
92 |
93 | The name of the Steam shortcut has not been set.
94 | A GlosSI target file already exists with the chosen shortcut name.
95 | The icon could not be found.
96 | The icon path leads to an executable (.exe) file. Any transparency will be lost.
97 |
98 |
99 | Failed to open the default target file: {0}
100 | Unexpected error encountered when reading the icon path: {0}
101 | Something went wrong when attempting to create the Steam shortcut: {0}
102 | Failed to check the GlosSI version: {0}
103 | Failed to access or create the default target file: {0}
104 | Failed to backup the GlosSI configuration files: {0}
105 | Failed to backup the shortcuts.vdf file: {0}
106 | Something went wrong when attempting to read the target file referenced by the {0} overlay name: {1}
107 | GlosSI Integration failed to run the Steam shortcut: {0}
108 | GlosSI Integration failed to add the GlosSI target configuration file and/or Steam shortcut for "{0}", the adding process was aborted: {1}
109 | GlosSI Integration failed to remove the GlosSI target configuration file and/or Steam shortcut for "{0}", the removal process was aborted: {1}
110 |
111 |
--------------------------------------------------------------------------------
/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/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/Files/GlosSITargetSettings.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Net.Http;
8 | using System.Threading.Tasks;
9 |
10 | namespace GlosSIIntegration.Models.GlosSITargets.Files
11 | {
12 | ///
13 | /// Represents the GlosSI target settings.
14 | ///
15 | /// See GlosSI Settings.h
16 | ///
17 | ///
18 | internal class GlosSITargetSettings
19 | {
20 | private static readonly HttpClient httpClient;
21 | private readonly string originalFilePath;
22 | private readonly JObject jObj;
23 | private const string nameKey = "name";
24 | public string Name
25 | {
26 | get => jObj.ToObject(nameKey);
27 | set => jObj.SetPropertyValue(nameKey, value);
28 | }
29 | private const string iconKey = "icon";
30 | ///
31 | /// Steam shortcut icon path.
32 | ///
33 | public string Icon
34 | {
35 | get => jObj.ToObject(iconKey);
36 | set => jObj.SetPropertyValue(iconKey, value);
37 | }
38 | private const string launchKey = "launch";
39 | public LaunchOptions Launch { get; set; }
40 |
41 | static GlosSITargetSettings()
42 | {
43 | httpClient = new HttpClient
44 | {
45 | BaseAddress = new Uri("http://127.0.0.1:8756/")
46 | };
47 | }
48 |
49 | private GlosSITargetSettings(JObject jObj, string originalFilePath)
50 | {
51 | this.jObj = jObj;
52 | this.originalFilePath = originalFilePath;
53 | Launch = jObj.ToObject(launchKey);
54 | }
55 |
56 | private GlosSITargetSettings(JObject jObj) : this(jObj, null)
57 | {
58 | originalFilePath = new GlosSITargetFileInfo(Name).FullPath;
59 | }
60 |
61 | public static async Task ReadFromAsync(string filePath)
62 | {
63 | return new GlosSITargetSettings(
64 | await JsonExtensions.ReadFromFileAsync(filePath).ConfigureAwait(false), filePath);
65 | }
66 |
67 | public static GlosSITargetSettings ReadFrom(string filePath)
68 | {
69 | return new GlosSITargetSettings(JsonExtensions.ReadFromFile(filePath), filePath);
70 | }
71 |
72 | private void RefreshJObj()
73 | {
74 | jObj.SetPropertyValue(launchKey, JToken.FromObject(Launch));
75 | }
76 |
77 | public void WriteTo()
78 | {
79 | WriteTo(originalFilePath);
80 | }
81 |
82 | public void WriteTo(string filePath)
83 | {
84 | RefreshJObj();
85 | jObj.WriteToFile(filePath);
86 | }
87 |
88 | public async Task WriteToAsync()
89 | {
90 | await WriteToAsync(originalFilePath).ConfigureAwait(false);
91 | }
92 |
93 | public async Task WriteToAsync(string filePath)
94 | {
95 | RefreshJObj();
96 | await jObj.WriteToFileAsync(filePath).ConfigureAwait(false);
97 | }
98 |
99 | ///
100 | /// Get the settings of the currently running GlosSITarget process.
101 | /// If GlosSITarget is not currently running, a will be thrown.
102 | ///
103 | /// The settings object.
104 | /// If, among other things, GlosSITarget is
105 | /// not currently running.
106 | public static async Task ReadCurrent()
107 | {
108 | using (HttpResponseMessage response = await httpClient.GetAsync("settings").ConfigureAwait(false))
109 | using (Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
110 | using (StreamReader streamReader = new StreamReader(stream))
111 | using (JsonTextReader jsonReader = new JsonTextReader(streamReader))
112 | {
113 | return new GlosSITargetSettings(
114 | (JObject)await JToken.ReadFromAsync(jsonReader).ConfigureAwait(false));
115 | }
116 | }
117 |
118 | ///
119 | /// Represents the launch options available to a GlosSI target.
120 | ///
121 | public class LaunchOptions // Includes every property as of GlosSI version 0.1.2.0.
122 | {
123 | ///
124 | /// Whether to launch the when the target is run.
125 | ///
126 | [JsonProperty("launch")]
127 | public bool Launch { get; set; }
128 | [JsonProperty("launchPath")]
129 | public string LaunchPath { get; set; }
130 | [JsonProperty("launchAppArgs")]
131 | public string LaunchAppArgs { get; set; }
132 | [JsonProperty("closeOnExit")]
133 | public bool CloseOnExit { get; set; }
134 | [JsonProperty("waitForChildProcs")]
135 | public bool WaitForChildProcs { get; set; }
136 | [JsonProperty("isUWP")]
137 | public bool IsUWP { get; set; }
138 | [JsonProperty("ignoreLauncher")]
139 | public bool IgnoreLauncher { get; set; }
140 | [JsonProperty("killLauncher")]
141 | public bool KillLauncher { get; set; }
142 | [JsonProperty("launcherProcesses")]
143 | public List LauncherProcesses { get; set; }
144 |
145 | ///
146 | /// Instantiates a object with default values such that
147 | /// everything is turned off and is set to false.
148 | ///
149 | public LaunchOptions()
150 | {
151 | Launch = false;
152 | LaunchPath = null;
153 | LaunchAppArgs = null;
154 | CloseOnExit = false;
155 | WaitForChildProcs = false;
156 | IsUWP = false;
157 | IgnoreLauncher = true;
158 | KillLauncher = false;
159 | LauncherProcesses = new List();
160 | }
161 |
162 | public bool IsEveryPropertyEqual(LaunchOptions other)
163 | {
164 | return Launch == other.Launch
165 | && LaunchPath == other.LaunchPath
166 | && LaunchAppArgs == other.LaunchAppArgs
167 | && CloseOnExit == other.CloseOnExit
168 | && WaitForChildProcs == other.WaitForChildProcs
169 | && IsUWP == other.IsUWP
170 | && IgnoreLauncher == other.IgnoreLauncher
171 | && KillLauncher == other.KillLauncher
172 | && (
173 | LauncherProcesses == other.LauncherProcesses
174 | || LauncherProcesses != null
175 | && other.LauncherProcesses != null
176 | && LauncherProcesses.SequenceEqual(other.LauncherProcesses)
177 | );
178 | }
179 |
180 | public override string ToString()
181 | {
182 | return JToken.FromObject(this).ToString();
183 | }
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/Source/Models/GlosSITargets/Files/JsonExtensions.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 |
6 | namespace GlosSIIntegration.Models.GlosSITargets.Files
7 | {
8 | internal static class JsonExtensions
9 | {
10 | public static async Task ReadFromFileAsync(string filePath)
11 | {
12 | using (StreamReader file = File.OpenText(filePath))
13 | using (JsonTextReader reader = new JsonTextReader(file))
14 | {
15 | return (JObject)await JToken.ReadFromAsync(reader).ConfigureAwait(false);
16 | }
17 | }
18 |
19 | public static JObject ReadFromFile(string filePath)
20 | {
21 | using (StreamReader file = File.OpenText(filePath))
22 | using (JsonTextReader reader = new JsonTextReader(file))
23 | {
24 | return (JObject)JToken.ReadFrom(reader);
25 | }
26 | }
27 |
28 | public static async Task WriteToFileAsync(this JObject o, string filePath)
29 | {
30 | using (StreamWriter file = File.CreateText(filePath))
31 | using (JsonTextWriter writer = new JsonTextWriter(file)
32 | {
33 | // Use the same indentation GlosSI uses, for consistency.
34 | Formatting = Formatting.Indented,
35 | Indentation = 4
36 | })
37 | {
38 | await o.WriteToAsync(writer).ConfigureAwait(false);
39 | // Write a final new line simply to stay consistent with GlosSI.
40 | await file.WriteLineAsync().ConfigureAwait(false);
41 | }
42 | }
43 |
44 | public static void WriteToFile(this JObject o, string filePath)
45 | {
46 | using (StreamWriter file = File.CreateText(filePath))
47 | using (JsonTextWriter writer = new JsonTextWriter(file)
48 | {
49 | // Use the same indentation GlosSI uses, for consistency.
50 | Formatting = Formatting.Indented,
51 | Indentation = 4
52 | })
53 | {
54 | o.WriteTo(writer);
55 | // Write a final new line simply to stay consistent with GlosSI.
56 | file.WriteLine();
57 | }
58 | }
59 |
60 | ///
61 | /// Sets the value of a property, by replacing its value or adding the token if it does not exist.
62 | /// Note: Does not handle nested .
63 | ///
64 | /// The JObject to modify.
65 | /// The name of the property to set the value of.
66 | /// The new value.
67 | public static void SetPropertyValue(this JObject o, string propertyName, JToken value)
68 | {
69 | JToken propertyToken = o.SelectToken(propertyName);
70 | if (propertyToken == null)
71 | {
72 | o.Add(propertyName, value);
73 | }
74 | else
75 | {
76 | propertyToken.Replace(value);
77 | }
78 | }
79 |
80 | ///
81 | /// Creates an instance of the specified type from the
82 | /// belonging to the .
83 | ///
84 | /// The object type that the token will be deserialized to.
85 | /// The JToken to access the property from.
86 | /// The name of the property to get the value of.
87 | /// The deserialized object, or null if deserialization failed.
88 | public static T ToObject(this JToken o, string propertyName) where T : class
89 | {
90 | try
91 | {
92 | return o.SelectToken(propertyName)?.ToObject();
93 | }
94 | catch (JsonReaderException ex)
95 | {
96 | Playnite.SDK.LogManager.GetLogger().Trace(ex,
97 | $"Failed to read JSON property {propertyName}.");
98 | return null;
99 | }
100 | catch (JsonSerializationException ex)
101 | {
102 | Playnite.SDK.LogManager.GetLogger().Trace(ex,
103 | $"Failed to deserialize JSON property {propertyName}.");
104 | return null;
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/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/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/GlosSITargets/Shortcuts/Crc.cs:
--------------------------------------------------------------------------------
1 | // CRC calculation utility. "pycrc" originally written by Thomas Pircher in Python.
2 | // The code has been translated to C# and stripped of some features.
3 | // See below for the original Python code copyright notice.
4 | //
5 | // Copyright (c) 2006-2013 Thomas Pircher
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to
9 | // deal in the Software without restriction, including without limitation the
10 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11 | // sell copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23 | // IN THE SOFTWARE.
24 |
25 | using System;
26 |
27 | namespace GlosSIIntegration.Models.GlosSITargets.Shortcuts
28 | {
29 | ///
30 | /// A base class for CRC routines.
31 | ///
32 | public class Crc
33 | {
34 | private readonly int width;
35 | private readonly bool reflectIn, reflectOut;
36 | private readonly uint xorOut, poly;
37 | private readonly uint msbMask, mask;
38 | private readonly uint nonDirectInit;
39 |
40 | public Crc(int width, uint poly, bool reflectIn, uint xorIn, bool reflectOut, uint xorOut)
41 | {
42 | this.width = width;
43 | this.poly = poly;
44 | this.reflectIn = reflectIn;
45 | this.reflectOut = reflectOut;
46 | this.xorOut = xorOut;
47 | this.msbMask = 0x1U << this.width - 1;
48 | this.mask = this.msbMask - 1 << 1 | 1;
49 | this.nonDirectInit = GetNondirectInit(xorIn);
50 | }
51 |
52 | ///
53 | /// Returns the non-direct init if the direct algorithm has been selected.
54 | ///
55 | private uint GetNondirectInit(uint init)
56 | {
57 | uint crc = init;
58 |
59 | for (int i = 0; i < this.width; i++)
60 | {
61 | uint bit = crc & 0x01;
62 | if (bit != 0)
63 | {
64 | crc ^= this.poly;
65 | }
66 | crc >>= 1;
67 | if (bit != 0)
68 | {
69 | crc |= this.msbMask;
70 | }
71 | }
72 | return crc & this.mask;
73 | }
74 |
75 | ///
76 | /// Reflect a data word, i.e. reverts the bit order.
77 | ///
78 | public uint Reflect(uint data, int width)
79 | {
80 | uint x = data & 0x01;
81 |
82 | for (int i = 0; i < width - 1; i++)
83 | {
84 | data >>= 1;
85 | x = x << 1 | data & 0x01;
86 | }
87 | return x;
88 | }
89 |
90 | ///
91 | /// Classic simple and slow CRC implementation.
92 | /// This function iterates bit by bit over the augmented input message and returns
93 | /// the calculated CRC value at the end.
94 | ///
95 | public uint BitByBit(string input)
96 | {
97 | bool topbit;
98 | uint register = this.nonDirectInit;
99 |
100 | foreach (char c in input)
101 | {
102 | uint octet = c;
103 | if (this.reflectIn) octet = Reflect(octet, 8);
104 | for (int i = 0; i < 8; i++)
105 | {
106 | topbit = Convert.ToBoolean(register & this.msbMask);
107 | register = register << 1 & this.mask | octet >> 7 - i & 0x01;
108 | if (topbit)
109 | {
110 | register ^= this.poly;
111 | }
112 | }
113 | }
114 | for (int i = 0; i < this.width; i++)
115 | {
116 | topbit = Convert.ToBoolean(register & this.msbMask);
117 | register = register << 1 & this.mask;
118 | if (topbit)
119 | {
120 | register ^= this.poly;
121 | }
122 | }
123 | if (this.reflectOut) register = Reflect(register, this.width);
124 | return register ^ this.xorOut;
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/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/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/GlosSITargets/TargetsVersionMigrator.cs:
--------------------------------------------------------------------------------
1 | using GlosSIIntegration.Models.GlosSITargets.Types;
2 | using Playnite.SDK;
3 | using Playnite.SDK.Models;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Windows;
7 |
8 | namespace GlosSIIntegration.Models
9 | {
10 | internal static class TargetsVersionMigrator
11 | {
12 | private static readonly ILogger logger = LogManager.GetLogger();
13 |
14 | ///
15 | /// Attempts to migrate targets to the latest version, if possible.
16 | ///
17 | /// The last "Targets.json" version,
18 | /// or 0 if there was no earlier version.
19 | public static void TryMigrate(int lastVersion)
20 | {
21 | if (lastVersion == 0)
22 | {
23 |
24 |
25 | List targetsToMigrate = GetTargetsToMigrate();
26 | if (targetsToMigrate.Count == 0)
27 | {
28 | logger.Info("The extension has not been used before: " +
29 | "target settings migration is not needed.");
30 | }
31 | else if (string.IsNullOrEmpty(GlosSIIntegration.GetSettings().SteamShortcutsPath))
32 | {
33 | throw new InvalidOperationException("Cannot migrate settings: The path to Steam has not been set.");
34 | }
35 | else
36 | {
37 | logger.Info("Migrating: Overwriting all target settings.");
38 | MigrateOverwriteTargetSettings(targetsToMigrate);
39 | }
40 | }
41 | }
42 |
43 | private static List GetTargetsToMigrate()
44 | {
45 | List targets = new List();
46 |
47 | if (PlayniteGlosSITarget.Exists())
48 | {
49 | targets.Add(new PlayniteGlosSITarget());
50 | }
51 |
52 | if (DefaultGlosSITarget.Exists())
53 | {
54 | targets.Add(new DefaultGlosSITarget());
55 | }
56 |
57 | foreach (Game game in GlosSIIntegration.Api.Database.Games)
58 | {
59 | if (GlosSIIntegration.GameHasIntegratedTag(game))
60 | {
61 | targets.Add(new GameGlosSITarget(game));
62 | }
63 | }
64 |
65 | return targets;
66 | }
67 |
68 | private static void MigrateOverwriteTargetSettings(List targetsToMigrate)
69 | {
70 | GlosSIIntegration.GetSettings().CreateDefaultTarget();
71 |
72 | ShowMigrationMessage();
73 |
74 | GlobalProgressResult result = GlosSIIntegration.Api.Dialogs.ActivateGlobalProgress(
75 | (progressBar) => OverwriteTargetSettings(progressBar, targetsToMigrate),
76 | new GlobalProgressOptions("Updating GlosSI Targets...", false)
77 | {
78 | IsIndeterminate = false
79 | });
80 |
81 | if (result.Error != null)
82 | {
83 | throw new Exception("Failed to overwrite target settings!", result.Error);
84 | }
85 | }
86 |
87 | private static void OverwriteTargetSettings(GlobalProgressActionArgs progressBar, List targets)
88 | {
89 | progressBar.ProgressMaxValue = targets.Count;
90 |
91 | foreach (GlosSITarget target in targets)
92 | {
93 | if (target.File.Exists())
94 | {
95 | target.File.Overwrite();
96 | }
97 | else
98 | {
99 | logger.Warn($"Could not find the \"{target.Name}\" target file.");
100 | }
101 | progressBar.CurrentProgressValue++;
102 | }
103 | }
104 |
105 | private static void ShowMigrationMessage()
106 | {
107 | // Most strings are not localized here,
108 | // since the migration message will be shown to users only once, most likely before the strings are ever localized.
109 | const string message =
110 | "The new version of the GlosSI Integration extension adds support for running the shortcuts created by this extension from Steam. " +
111 | "Additionally, if you use the Playnite library overlay, you can now switch between Steam and fullscreen Playnite. " +
112 | "Start the Playnite overlay shortcut from Steam to enter Playnite. " +
113 | "Exit the Playnite overlay (via the Steam overlay) to switch to Steam Big Picture mode.\r\n\r\n" +
114 | "The settings of the currently existing GlosSI targets have to be updated and " +
115 | "Playnite game images will be added to the Steam shortcuts (when possible). " +
116 | "Click the OK button to update all targets used by this extension. " +
117 | "The default settings have also been updated: " +
118 | "if you know what you are doing and want to change them before proceeding, click the \"Review DefaultTarget.json\" button.";
119 |
120 | List options = new List
121 | {
122 | new MessageBoxOption("Review DefaultTarget.json", false, false),
123 | new MessageBoxOption(ResourceProvider.GetString("LOCOKLabel"), true, true)
124 | };
125 | MessageBoxOption result = GlosSIIntegration.Api.Dialogs.ShowMessage(message,
126 | ResourceProvider.GetString("LOC_GI_DefaultWindowTitle") + " – Update notice", MessageBoxImage.Information, options);
127 |
128 | if (result == options[0])
129 | {
130 | GlosSIIntegrationSettingsView.OpenDefaultGlosSITarget();
131 | ShowMigrationMessage();
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/Models/Overlays/GlosSITargetProcess.cs:
--------------------------------------------------------------------------------
1 | using Playnite.SDK;
2 | using System;
3 | using System.Diagnostics;
4 | using System.Threading.Tasks;
5 |
6 | namespace GlosSIIntegration.Models.Overlays
7 | {
8 | ///
9 | /// Encapsulates static methods relating to a GlosSITarget process.
10 | ///
11 | /// Note that only one GlosSITarget process can run at a time, at least for any length of time.
12 | ///
13 | ///
14 | internal static class GlosSITargetProcess // TODO: Make instantiable! Also inherit from process?
15 | {
16 | private const string WindowClassName = "SFML_Window";
17 | private const string WindowName = "GlosSITarget";
18 | private const string ProcessName = WindowName;
19 | private static readonly ILogger logger = LogManager.GetLogger();
20 |
21 | ///
22 | /// Waits for GlosSITarget to start and returns the found process.
23 | ///
24 | /// The approximate timeout in milliseconds.
25 | /// The found GlosSITarget process.
26 | /// If GlosSITarget did not start after
27 | /// milliseconds.
28 | public static async Task WaitForProcessToStart(int timeout = 30000)
29 | {
30 | // TODO: Could be made non-polling by using the ManagementEventWatcher class.
31 |
32 | const int pollingDelay = 333;
33 | int sleptTime = 0;
34 | Process foundProcess;
35 |
36 | logger.Trace("Waiting for GlosSITarget to start...");
37 |
38 | while ((foundProcess = GetRunning()) == null)
39 | {
40 | await Task.Delay(pollingDelay).ConfigureAwait(false);
41 | if ((sleptTime += pollingDelay) > timeout)
42 | {
43 | const string errorMsg = "GlosSITarget did not start in time. " +
44 | "Ensure that the specified path to GlosSITarget is correct and has not been changed. " +
45 | "Also make sure that the Steam shortcut has not been renamed.";
46 | logger.Error(errorMsg);
47 | throw new TimeoutException(errorMsg);
48 | }
49 | }
50 |
51 | logger.Trace("GlosSITarget started.");
52 |
53 | return foundProcess;
54 | }
55 |
56 | ///
57 | /// Waits for the GlosSITarget window to start.
58 | ///
59 | /// The approximate timeout in milliseconds.
60 | /// If the window did not start after
61 | /// milliseconds.
62 | public static async Task WaitForWindowToStart(int timeout = 5000)
63 | {
64 | const int pollingDelay = 50;
65 | int sleptTime = 0;
66 |
67 | logger.Trace("Waiting for GlosSITarget window to open...");
68 |
69 | while (WinWindow.Find(WindowClassName, "GlosSITarget") == null)
70 | {
71 | await Task.Delay(pollingDelay).ConfigureAwait(false);
72 | if ((sleptTime += pollingDelay) > timeout)
73 | {
74 | throw new TimeoutException("GlosSITarget window did not open in time.");
75 | }
76 | }
77 |
78 | logger.Trace("GlosSITarget window opened.");
79 | }
80 |
81 | // TODO: Make static property. Or update the version requirement and remove this check?
82 | // GetSettings().GlosSIVersion is not exactly thread-safe, but it is unlikely to ever change...
83 | ///
84 | /// Checks if starting a new GlosSITarget process would automatically
85 | /// replace (i.e. close) any old process.
86 | /// If not, starting a new GlosSITarget process while another
87 | /// GlosSITarget process is running will simply result in the started
88 | /// GlosSITarget process closing itself.
89 | /// This depends the installed GlosSI version.
90 | ///
91 | /// True if starting a GlosSITarget process would automatically
92 | /// replace any currently running GlosSITarget process; false otherwise.
93 | public static bool DoesNewReplaceOld()
94 | {
95 | // No need to close any already running GlosSITarget process, if
96 | // launching GlosSITarget version >= v0.1.2.0,
97 | // since it should close any already running GlosSITarget process automatically.
98 | return GlosSIIntegration.GetSettings().GlosSIVersion >= new Version("0.1.2.0");
99 | }
100 |
101 | public static WinWindow FindWindow()
102 | {
103 | return WinWindow.Find(WindowClassName, WindowName);
104 | }
105 |
106 | ///
107 | /// Starts closing of any currently running GlosSITarget process.
108 | /// If no process to close was found, simply logs a warning.
109 | ///
110 | /// Note that GlosSITarget's window must have started before this method is called
111 | /// and that the process is not necessarily closed yet when the method returns.
112 | ///
113 | ///
114 | public static void Close()
115 | {
116 | // GlosSITarget's window must have started by now, should be ensured by WaitForWindowToStart().
117 | WinWindow window = FindWindow();
118 |
119 | if (window == null)
120 | {
121 | return;
122 | }
123 |
124 | try
125 | {
126 | // The below method is used instead of Process.CloseMainWindow()
127 | // because GlosSITarget is not guaranteed to have a main window.
128 | window.Close();
129 | }
130 | catch (InvalidOperationException ex)
131 | {
132 | logger.Warn(ex, ex.Message);
133 | }
134 | }
135 |
136 | ///
137 | /// Checks if a GlosSITarget process is currently running.
138 | ///
139 | ///
140 | public static bool IsRunning()
141 | {
142 | using (Process process = GetRunning())
143 | {
144 | return process != null;
145 | }
146 | }
147 |
148 | ///
149 | /// Gets any currently running GlosSITarget process.
150 | ///
151 | /// The currently running GlosSITarget process,
152 | /// or null if GlosSITarget is not currently running.
153 | public static Process GetRunning()
154 | {
155 | return ExtractSingleProcessFromArray(Process.GetProcessesByName(ProcessName));
156 | }
157 |
158 | ///
159 | /// Extracts the first process from an array of processes.
160 | /// Any additional processes are disposed and a warning is logged.
161 | ///
162 | /// The array of processes to pick the first from.
163 | /// A process from the array.
164 | private static Process ExtractSingleProcessFromArray(Process[] processes)
165 | {
166 | if (processes.Length == 0)
167 | {
168 | return null;
169 | }
170 |
171 | if (processes.Length > 1)
172 | {
173 | logger.Warn($"Multiple ({processes.Length}) processes were found.");
174 | for (int i = 1; i < processes.Length; i++) processes[i].Dispose();
175 | }
176 |
177 | return processes[0];
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/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/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/Models/Overlays/Types/GameOverlay.cs:
--------------------------------------------------------------------------------
1 | using GlosSIIntegration.Models.GlosSITargets.Types;
2 | using Playnite.SDK.Models;
3 | using System;
4 | using System.Diagnostics;
5 | using System.Threading.Tasks;
6 |
7 | namespace GlosSIIntegration.Models.Overlays.Types
8 | {
9 | internal class GameOverlay : SteamStartableOverlay, IDisposable
10 | {
11 | private Process runningGameProcess;
12 | private readonly object runningGameLock = new object();
13 | private readonly bool closeGameWhenOverlayClosed;
14 | public Game AssociatedGame { get; }
15 |
16 | ///
17 | /// Creates an overlay object for the game, even if the game should not actually have one.
18 | ///
19 | /// The game for which this overlay is used.
20 | /// If the path to GlosSI has not been set.
21 | protected GameOverlay(Game associatedGame) : this(associatedGame, new GameGlosSITarget(associatedGame)) { }
22 |
23 | ///
24 | /// Creates an overlay object for the game, even if the game should not actually have one.
25 | ///
26 | /// The game for which this overlay is used.
27 | /// The associated with the game.
28 | /// If the path to GlosSI has not been set.
29 | protected GameOverlay(Game associatedGame, GlosSITarget target) : base(target)
30 | {
31 | runningGameProcess = null;
32 | AssociatedGame = associatedGame;
33 | closeGameWhenOverlayClosed = GlosSIIntegration.GetSettings().CloseGameWhenOverlayIsClosed;
34 | }
35 |
36 | ///
37 | /// Creates the overlay associated with a game, if there is one.
38 | /// Note: This method does not check if the game has the ignored tag.
39 | ///
40 | /// The game to create the overlay for.
41 | /// The overlay if the game should have an overlay; null otherwise.
42 | public static GameOverlay Create(Game game)
43 | {
44 | try
45 | {
46 | if (GlosSIIntegration.GameHasIntegratedTag(game))
47 | {
48 | return new GameOverlay(game);
49 | }
50 | else if (GlosSIIntegration.GetSettings().UseDefaultOverlay
51 | && !GlosSIIntegration.IsSteamGame(game))
52 | {
53 | return new DefaultGameOverlay(game);
54 | }
55 | }
56 | catch (InvalidOperationException ex)
57 | {
58 | logger.Warn($"Cannot create game overlay: {ex.Message}");
59 | }
60 |
61 | return null;
62 | }
63 |
64 | ///
65 | /// Checks if references the same game as .
66 | ///
67 | /// The game to compare.
68 | /// true if the same game is referenced; false otherwise.
69 | public bool IsGameSame(Game game) // TODO: Remove and instead just use AssociatedGame.Equals() wherever needed?
70 | {
71 | if (game == null) return false;
72 |
73 | return game.Equals(AssociatedGame);
74 | }
75 |
76 | ///
77 | /// Associates a game process with this overlay.
78 | /// If this overlay is closed by the user, the attached game process will also be closed.
79 | ///
80 | /// The PID of the game process.
81 | public void AttachGameProcess(int gamePid)
82 | {
83 | lock (runningGameLock)
84 | {
85 | if (runningGameProcess != null)
86 | {
87 | logger.Error("Attempted to set game overlay game process twice.");
88 | return;
89 | }
90 |
91 | try
92 | {
93 | runningGameProcess = Process.GetProcessById(gamePid);
94 | }
95 | catch (ArgumentException)
96 | {
97 | logger.Warn($"{gamePid} is not a valid game PID to track");
98 | }
99 | catch (InvalidOperationException ex)
100 | {
101 | logger.Error(ex, "Failed to get game process by id.");
102 | }
103 | }
104 | }
105 |
106 | protected override async Task OnClosedCalled(int overlayExitCode)
107 | {
108 | Task baseTask = base.OnClosedCalled(overlayExitCode);
109 |
110 | try
111 | {
112 | // Note that runningGameProcess is not disposed here.
113 | // This is because the overlay may be reused for the same game.
114 |
115 | lock (runningGameLock)
116 | {
117 | // Trivia: The extension used to check the exit code of GlosSITarget
118 | // to avoid killing the game when the user closed the GlosSITarget
119 | // process normally (i.e. not via Steam).
120 | // However, Steam appears to have changed the way it closes games,
121 | // as such the exit code can no longer be relied on.
122 | // Before one could assume an exit code of 1 meant that the game
123 | // was closed via the Steam overlay,
124 | // unless something went wrong or the process was killed by other means.
125 | //
126 | // TODO: Add a new method of checking if GlosSITarget was closed via
127 | // the Steam overlay or not.
128 | if (closeGameWhenOverlayClosed &&
129 | // Check that the overlay was closed from the overlay/externally,
130 | // and not via the extension.
131 | !State.ClosedByExtension &&
132 | // If the game has already closed, there is no need to close it.
133 | !runningGameProcess.HasExitedSafe() &&
134 | // If a GlosSITarget is running, the overlay was probably replaced by another
135 | // GlosSITarget process, and closing the game would be unexpected.
136 | !GlosSITargetProcess.IsRunning())
137 | {
138 | if (runningGameProcess == null)
139 | {
140 | logger.Warn("Game overlay has no attached game process.");
141 | return;
142 | }
143 | KillGame();
144 | }
145 | }
146 | }
147 | finally
148 | {
149 | await baseTask.ConfigureAwait(false);
150 | }
151 | }
152 |
153 | private void KillGame()
154 | {
155 | try
156 | {
157 | // TODO: Might want to close the game more gracefully? At least as an alternative?
158 | // TODO: This does not work for all games, as the PID reported might not be the actual game exe, but rather an auxillary exe.
159 | // Some kind of heuristic is needed.
160 | logger.Debug($"Killing game (with PID {runningGameProcess.Id}) in retaliation for GlosSI being killed.");
161 | runningGameProcess.Kill();
162 | }
163 | catch (InvalidOperationException ex)
164 | {
165 | logger.Warn($"Game closed before GlosSITarget, not doing anything: {ex.Message}.");
166 | }
167 | catch (Exception ex)
168 | when (ex is InvalidOperationException || ex is System.ComponentModel.Win32Exception)
169 | {
170 | logger.Error(ex, "Killing game failed:");
171 | }
172 | finally
173 | {
174 | runningGameProcess.Dispose();
175 | runningGameProcess = null;
176 | }
177 | }
178 |
179 | ///
180 | /// Disposes the game process held by this object, if any.
181 | /// This method can safely be called at any time: the object remains usable afterwards:
182 | /// it simply becomes unable to close the currently running game when the overlay is closed.
183 | ///
184 | public void Dispose()
185 | {
186 | lock (runningGameLock)
187 | {
188 | if (runningGameProcess != null)
189 | {
190 | runningGameProcess.Dispose();
191 | runningGameProcess = null;
192 | }
193 | }
194 | }
195 |
196 | protected override void OnStartedCalled() { }
197 |
198 | protected override void BeforeClosedCalled() { }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Source/Models/Overlays/Types/Overlay.cs:
--------------------------------------------------------------------------------
1 | using GlosSIIntegration.Models.GlosSITargets.Types;
2 | using Playnite.SDK;
3 | using System;
4 | using System.Diagnostics;
5 | using System.Threading.Tasks;
6 |
7 | namespace GlosSIIntegration.Models.Overlays.Types
8 | {
9 | internal abstract class Overlay
10 | {
11 | protected static readonly ILogger logger = LogManager.GetLogger();
12 |
13 | ///
14 | /// The GlosSI target associated with the overlay.
15 | ///
16 | public GlosSITarget Target { get; }
17 |
18 | ///
19 | /// If needs to be accessed from a separate thread or modified,
20 | /// use this lock to ensure thread safe access to it.
21 | ///
22 | public readonly object stateLock = new object();
23 | private MutableOverlayState state;
24 | ///
25 | /// Reset when a new overlay is started.
26 | /// null if no overlay has ever started.
27 | /// GlosSITargetProcess property is null when the overlay has exited
28 | /// or access to the process has been relinquished to a different overlay.
29 | ///
30 | public IOverlayState State => state;
31 |
32 |
33 | ///
34 | /// Instantiates a new overlay object.
35 | ///
36 | /// The object should be "disposed" of by calling
37 | /// or by having it replaced using .
38 | ///
39 | ///
40 | /// If the path to GlosSI
41 | /// has not been set.
42 | protected Overlay(GlosSITarget target)
43 | {
44 | Target = target;
45 | state = null;
46 | }
47 |
48 | ///
49 | /// Checks if an overlay starts the same Steam shortcut as this process was started from.
50 | ///
51 | /// The other overlay to compare with.
52 | /// True if both overlays start the same Steam shortcut; false otherwise.
53 | public bool StartsSameShortcutAs(Overlay otherOverlay)
54 | {
55 | return Target.Equals(otherOverlay.Target);
56 | }
57 |
58 | protected internal async Task BeforeStarted()
59 | {
60 | lock (stateLock)
61 | {
62 | state = new MutableOverlayState()
63 | {
64 | StartedByExtension = true
65 | };
66 | }
67 |
68 | await BeforeStartedCalled().ConfigureAwait(false);
69 |
70 | // Try to prevent GlosSITarget from taking focus when it starts.
71 | // Note: This assumes that no game is started between BeforeOverlayClosed() and OnOverlayClosed().
72 | if (!WinWindow.LockSetForegroundWindow())
73 | {
74 | logger.Warn("Failed to prevent GlosSITarget from stealing focus: LockSetForegroundWindow failed.");
75 | }
76 | }
77 |
78 | protected abstract Task BeforeStartedCalled();
79 |
80 | ///
81 | /// Called when the overlay process and window has started.
82 | /// Updates the property.
83 | ///
84 | /// The process of the started overlay.
85 | protected internal void OnStarted(Process startedOverlay)
86 | {
87 | lock (stateLock)
88 | {
89 | state.GlosSITargetProcess = startedOverlay;
90 | }
91 | WinWindow.UnlockSetForegroundWindow();
92 | OnStartedCalled();
93 | }
94 |
95 | // Note: Not called for overlays not started by the extension.
96 | protected abstract void OnStartedCalled();
97 |
98 | ///
99 | /// Called if the overlay was started externally.
100 | ///
101 | /// The overlay process.
102 | protected void StartedExternally(Process startedOverlay)
103 | {
104 | lock (stateLock)
105 | {
106 | state = new MutableOverlayState()
107 | {
108 | GlosSITargetProcess = startedOverlay
109 | };
110 | }
111 | }
112 |
113 | ///
114 | /// Called when the overlay is about to be purposefully closed.
115 | /// The method should not be called if the overlay has already closed
116 | /// or is closed externally.
117 | ///
118 | protected internal void BeforeClosed()
119 | {
120 | lock (stateLock)
121 | {
122 | state.ClosedByExtension = true;
123 | }
124 | BeforeClosedCalled();
125 | }
126 |
127 | protected abstract void BeforeClosedCalled();
128 |
129 | // TODO: Remove the now unused exit code parameter.
130 | ///
131 | /// Called when the overlay process has closed. Disposes resources, if necessary.
132 | ///
133 | /// The exit code of the overlay process.
134 | protected internal async Task OnClosed(int overlayExitCode) {
135 | try
136 | {
137 | await OnClosedCalled(overlayExitCode).ConfigureAwait(false);
138 | }
139 | finally
140 | {
141 | lock (stateLock)
142 | {
143 | state.GlosSITargetProcess?.Dispose();
144 | state.GlosSITargetProcess = null;
145 | }
146 | }
147 | }
148 |
149 | protected abstract Task OnClosedCalled(int overlayExitCode);
150 |
151 | ///
152 | /// Called when this overlay replaces an old overlay.
153 | /// By default, moves the process from the old overlay to this overlay.
154 | /// Note: The overlay should not be replaced between BeforeStarted and OnStarted, BeforeClosed and OnClosed
155 | /// Note: The old overlay should be considered as simply having been newly instantiated:
156 | /// its state is left untouched.
157 | ///
158 | /// The old overlay that this overlay replaces.
159 | /// The new overlay that replaces the old overlay (i.e. this overlay).
160 | protected internal Overlay Replaces(Overlay otherOverlay)
161 | {
162 | if (this != otherOverlay)
163 | {
164 | lock (stateLock)
165 | {
166 | if (state?.GlosSITargetProcess != null)
167 | {
168 | // Should not happen.
169 | throw new InvalidOperationException("Tried to replace a running overlay " +
170 | "with another supposedly running overlay: this should be impossible.");
171 | }
172 |
173 | lock (otherOverlay.stateLock)
174 | {
175 | state = new MutableOverlayState(otherOverlay.state);
176 | // The other overlay may no longer mess with the process.
177 | otherOverlay.state.GlosSITargetProcess = null;
178 | }
179 | }
180 | }
181 | else
182 | {
183 | // Should not happen?
184 | logger.Warn("Tried to replace an overlay with the same overlay.");
185 | }
186 |
187 | return this;
188 | }
189 |
190 | public interface IOverlayState
191 | {
192 | ///
193 | /// The GlosSITarget process associated with the overlay, if any.
194 | /// If null, the overlay no longer owns the process,
195 | /// the process has been closed or it was never ran to begin with.
196 | ///
197 | Process GlosSITargetProcess { get; }
198 | bool StartedByExtension { get; }
199 | bool ClosedByExtension { get; }
200 | }
201 |
202 | private class MutableOverlayState : IOverlayState
203 | {
204 | public Process GlosSITargetProcess { get; set; }
205 | public bool StartedByExtension { get; set; }
206 | public bool ClosedByExtension { get; set; }
207 |
208 | public MutableOverlayState()
209 | {
210 | GlosSITargetProcess = null;
211 | StartedByExtension = false;
212 | ClosedByExtension = false;
213 | }
214 |
215 | public MutableOverlayState(IOverlayState otherState)
216 | {
217 | GlosSITargetProcess = otherState.GlosSITargetProcess;
218 | StartedByExtension = otherState.StartedByExtension;
219 | ClosedByExtension = otherState.ClosedByExtension;
220 | }
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Source/Models/PlayniteGameSteamAssets.cs:
--------------------------------------------------------------------------------
1 | using GlosSIIntegration.Models.GlosSITargets.Shortcuts;
2 | using Playnite.SDK;
3 | using Playnite.SDK.Models;
4 | using System;
5 | using System.Drawing;
6 | using System.IO;
7 | using System.Windows.Media.Imaging;
8 |
9 | namespace GlosSIIntegration.Models
10 | {
11 | internal class PlayniteGameSteamAssets : SteamGameAssets // TODO: Composition instead?
12 | {
13 | private static readonly ILogger logger = LogManager.GetLogger();
14 | private readonly Game game;
15 | public PlayniteGameSteamAssets(Game playniteGame, SteamShortcut steamShortcut) : base(steamShortcut)
16 | {
17 | game = playniteGame;
18 | }
19 |
20 | ///
21 | /// Gets the Playnite logo path, from darklinkpower's Extra Metadata Loader extension.
22 | /// Note that the path to the file returned by this method might not actually exist.
23 | ///
24 | /// The path to where a Playnite logo file could be found.
25 | private string GetPlayniteLogoPath()
26 | {
27 | return Path.Combine(GlosSIIntegration.Instance.PlayniteApi.Paths.ConfigurationPath,
28 | "ExtraMetadata",
29 | "games",
30 | game.Id.ToString(),
31 | "Logo.png"); // Always a .png file.
32 | }
33 |
34 | private string GetPlayniteCoverPath()
35 | {
36 | return (game.CoverImage == null) ? null :
37 | API.Instance.Database.GetFullFilePath(game.CoverImage);
38 | }
39 |
40 | private string GetPlayniteBackgroundPath()
41 | {
42 | return (game.BackgroundImage == null) ? null :
43 | API.Instance.Database.GetFullFilePath(game.BackgroundImage);
44 | }
45 |
46 | // Note: Since Playnite does not have a concept of
47 | // separate horizontal and vertical cover images (grids),
48 | // only one can be set when calling this method.
49 | // Missing images would have to be fetched from elsewhere.
50 | public void SetFromPlayniteAssets(bool overwrite = false)
51 | {
52 | TrySetAsset(GetPlayniteCoverPath(), overwrite, SetGrid);
53 | TrySetAsset(GetPlayniteBackgroundPath(), overwrite, SetHero);
54 | TrySetAsset(GetPlayniteLogoPath(), overwrite, SetLogo);
55 | }
56 |
57 | private void TrySetAsset(string filePath, bool overwrite, Action setImageAction)
58 | {
59 | if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) return;
60 |
61 | string fileExtension = Path.GetExtension(filePath);
62 | if (Asset.HasValidFileExtension(fileExtension))
63 | {
64 | setImageAction(filePath, overwrite);
65 | }
66 | else
67 | {
68 | logger.Warn($"Could not add shortcut image from Playnite image: " +
69 | $"Steam does not support the file extension of the file " +
70 | $"\"{Path.GetFileName(filePath)}\".");
71 | }
72 | }
73 |
74 | ///
75 | /// Reads the size of an image.
76 | ///
77 | /// The path to the image.
78 | /// The size in pixels.
79 | /// If reading the size of the file
80 | /// is not supported.
81 | private static Size GetImageSize(string filePath)
82 | {
83 | using (FileStream fileStream = File.OpenRead(filePath))
84 | {
85 | BitmapFrame bitmapFrame = BitmapFrame.Create(fileStream,
86 | BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
87 | return new Size(bitmapFrame.PixelWidth, bitmapFrame.PixelHeight);
88 | }
89 | }
90 |
91 | private void SetVerticalGrid(string filePath, bool overwrite)
92 | {
93 | if (overwrite || VerticalGrid.CurrentPath == null)
94 | {
95 | VerticalGrid.CurrentPath = filePath;
96 | }
97 | }
98 |
99 | private void SetHorizontalGrid(string filePath, bool overwrite)
100 | {
101 | if (overwrite || HorizontalGrid.CurrentPath == null)
102 | {
103 | HorizontalGrid.CurrentPath = filePath;
104 | }
105 | }
106 |
107 | private void SetHero(string filePath, bool overwrite)
108 | {
109 | if (overwrite || Hero.CurrentPath == null)
110 | {
111 | Hero.CurrentPath = filePath;
112 | }
113 | }
114 |
115 | private void SetLogo(string filePath, bool overwrite)
116 | {
117 | if (overwrite || Logo.CurrentPath == null)
118 | {
119 | Logo.CurrentPath = filePath;
120 | }
121 | }
122 |
123 |
124 | // TODO: Compare aspect ratios and overwrite the file if the
125 | // new image has a more conformant aspect ratio?
126 | ///
127 | /// Tries to set the vertical or horizontal grid image, depending on if the image is more tall or wide.
128 | /// Does nothing if the image is perfectly square.
129 | ///
130 | /// The path to the image file.
131 | /// true if any existing asset should be overwritten; false otherwise.
132 | private void SetGrid(string filePath, bool overwrite)
133 | {
134 | Size imageSize;
135 |
136 | try
137 | {
138 | imageSize = GetImageSize(filePath);
139 | }
140 | catch (NotSupportedException ex)
141 | {
142 | logger.Warn(ex, "Could not read the size of the image: not adding Steam shortcut image.");
143 | return;
144 | }
145 |
146 | if (imageSize.Height > imageSize.Width)
147 | {
148 | SetVerticalGrid(filePath, overwrite);
149 | }
150 | else if (imageSize.Width > imageSize.Height)
151 | {
152 | SetHorizontalGrid(filePath, overwrite);
153 | }
154 | else
155 | {
156 | // Square, will not look good on both grids.
157 | logger.Info($"The grid image of the Playnite game \"{game.Name}\" is square " +
158 | $"and will not be used for the Steam shortcut.");
159 | }
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/Source/Models/ProcessExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 | using System.Runtime.InteropServices;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace GlosSIIntegration.Models
9 | {
10 | internal static class ProcessExtensions
11 | {
12 | #region Win32
13 | ///
14 | /// Exit code if a process has not terminated
15 | /// (unless the process exited with this exit code).
16 | ///
17 | private const int StillActiveExitCode = 259;
18 | [Flags]
19 | public enum ProcessAccessFlags : uint
20 | {
21 | ProcessQueryLimitedInformation = 0x00001000,
22 | }
23 |
24 | [DllImport("kernel32.dll", SetLastError = true)]
25 | private static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, int dwProcessId);
26 | [DllImport("kernel32.dll", SetLastError = true)]
27 | private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
28 | [DllImport("kernel32.dll", SetLastError = true)]
29 | private static extern bool CloseHandle(IntPtr handle);
30 | #endregion
31 |
32 | ///
33 | /// Checks if a process has exited. Replaces .
34 | /// Unlike , this method will not throw access denied
35 | /// when the process is run with elevated privileges.
36 | /// See this webpage
37 | /// for details regarding the bug.
38 | /// Note that this method is currently simpler and not as robust as ,
39 | /// since it only checks if the exit code is not 259.
40 | ///
41 | /// The process to check if it has exited.
42 | /// True if the process has exited; false otherwise.
43 | /// If something went wrong...
44 | public static bool HasExitedSafe(this Process process)
45 | {
46 | IntPtr hProcess = OpenProcess(ProcessAccessFlags.ProcessQueryLimitedInformation, false, process.Id);
47 |
48 | if (hProcess == null)
49 | {
50 | throw new Win32Exception();
51 | }
52 |
53 | if (!GetExitCodeProcess(hProcess, out uint exitCode))
54 | {
55 | CloseHandle(hProcess);
56 | throw new Win32Exception();
57 | }
58 |
59 | if (!CloseHandle(hProcess))
60 | {
61 | throw new Win32Exception();
62 | }
63 |
64 | return exitCode != StillActiveExitCode;
65 | }
66 |
67 | // Method stolen from
68 | // https://github.com/microsoft/vs-threading/blob/main/src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs
69 | // Changed to use HasExitedSafe() instead.
70 | /* Original license:
71 | Microsoft.VisualStudio.Threading
72 | Copyright (c) Microsoft Corporation
73 | All rights reserved.
74 |
75 | MIT License
76 |
77 | Permission is hereby granted, free of charge, to any person obtaining a copy
78 | of this software and associated documentation files (the "Software"), to deal
79 | in the Software without restriction, including without limitation the rights
80 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
81 | copies of the Software, and to permit persons to whom the Software is
82 | furnished to do so, subject to the following conditions:
83 |
84 | The above copyright notice and this permission notice shall be included in all
85 | copies or substantial portions of the Software.
86 |
87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
90 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
92 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
93 | SOFTWARE.
94 | */
95 | ///
96 | /// Returns a task that completes when the process exits and provides the exit code of that process.
97 | ///
98 | /// The process to wait for exit.
99 | ///
100 | /// A token whose cancellation will cause the returned Task to complete
101 | /// before the process exits in a faulted state with an .
102 | /// This token has no effect on the itself.
103 | ///
104 | /// A task whose result is the of the .
105 | public static async Task WaitForExitAsyncSafe(this Process process, CancellationToken cancellationToken = default)
106 | {
107 | TaskCompletionSource tcs = new TaskCompletionSource();
108 |
109 | void exitHandler(object s, EventArgs e)
110 | {
111 | tcs.TrySetResult(process.ExitCode);
112 | }
113 |
114 | try
115 | {
116 | process.EnableRaisingEvents = true;
117 | process.Exited += exitHandler;
118 | if (process.HasExitedSafe()) // TODO: Could directly get the exit code from HasExitedSafe() here...
119 | {
120 | // Allow for the race condition that the process has already exited.
121 | tcs.TrySetResult(process.ExitCode);
122 | }
123 |
124 | using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
125 | {
126 | return await tcs.Task.ConfigureAwait(false);
127 | }
128 | }
129 | finally
130 | {
131 | process.Exited -= exitHandler;
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Source/Models/SteamGameAssets.cs:
--------------------------------------------------------------------------------
1 | using GlosSIIntegration.Models.GlosSITargets.Shortcuts;
2 | using Playnite.SDK;
3 | using System;
4 | using System.ComponentModel;
5 | using System.IO;
6 | using System.Linq;
7 |
8 | namespace GlosSIIntegration.Models
9 | {
10 | internal class SteamGameAssets
11 | {
12 | public Asset VerticalGrid { get; }
13 | public Asset HorizontalGrid { get; }
14 | public Asset Logo { get; }
15 | public Asset Hero { get; }
16 |
17 | public SteamGameAssets(SteamShortcut steamShortcut)
18 | {
19 | uint gameId = (uint)(steamShortcut.Id >> 32);
20 | VerticalGrid = new VerticalGridAsset(gameId);
21 | HorizontalGrid = new HorizontalGridAsset(gameId);
22 | Logo = new LogoAsset(gameId);
23 | Hero = new HeroAsset(gameId);
24 | }
25 |
26 | public virtual void DeleteAllAssets()
27 | {
28 | VerticalGrid.CurrentPath = null;
29 | HorizontalGrid.CurrentPath = null;
30 | Hero.CurrentPath = null;
31 | Logo.CurrentPath = null;
32 | }
33 |
34 | private class VerticalGridAsset : Asset
35 | {
36 | public VerticalGridAsset(uint gameId) : base(gameId, "p") { }
37 | }
38 |
39 | private class HorizontalGridAsset : Asset
40 | {
41 | public HorizontalGridAsset(uint gameId) : base(gameId, "") { }
42 | }
43 |
44 | private class LogoAsset : Asset
45 | {
46 | public LogoAsset(uint gameId) : base(gameId, "_logo") { }
47 | }
48 |
49 | private class HeroAsset : Asset
50 | {
51 | public HeroAsset(uint gameId) : base(gameId, "_hero") { }
52 | }
53 |
54 | public abstract class Asset
55 | {
56 | private static readonly ILogger logger = LogManager.GetLogger();
57 | // TODO: Support more common file extensions by converting those files.
58 | // Steam seems to also support .bmp images if renamed to one of the SupportedFileExtensions.
59 | ///
60 | /// Image file extensions supported by Steam.
61 | ///
62 | private static readonly string[] SupportedFileExtensions = { ".jpg", ".jpeg", ".png" };
63 | private readonly string fileNameWithoutExtension;
64 | private readonly string gridDirectoryPath;
65 | private string currentImagePath;
66 |
67 | ///
68 | /// The current path to the Steam asset.
69 | /// Set it to update the image: the file in the provided path will be copied
70 | /// (or, if possible, hard linked).
71 | /// Set it to null to simply delete the image.
72 | ///
73 | /// If set to a path with
74 | /// an unsupported file extension.
75 | public string CurrentPath
76 | {
77 | get => currentImagePath;
78 | set
79 | {
80 | if (currentImagePath != null)
81 | {
82 | DeleteCurrentImage();
83 | }
84 |
85 | if (value != null)
86 | {
87 | SetCurrentImage(value);
88 | }
89 | }
90 | }
91 |
92 | ///
93 | /// Refreshes the value of ,
94 | /// in case it was changed from outside this object.
95 | ///
96 | /// If this object is kept in memory for longer duration,
97 | /// consider calling this method before accessing .
98 | ///
99 | ///
100 | public void RefreshCurrentImagePath()
101 | {
102 | currentImagePath = GetExistingAssetPath();
103 | }
104 |
105 | protected Asset(uint gameId, string suffix)
106 | {
107 | fileNameWithoutExtension = gameId.ToString() + suffix;
108 | gridDirectoryPath = GetGridDirectoryPath();
109 | RefreshCurrentImagePath();
110 | }
111 |
112 | private void DeleteCurrentImage()
113 | {
114 | File.Delete(currentImagePath);
115 | logger.Info($"Deleted file \"{currentImagePath}\".");
116 | currentImagePath = null;
117 | }
118 |
119 | private void SetCurrentImage(string filePath)
120 | {
121 | string fileExtension = Path.GetExtension(filePath);
122 | if (!HasValidFileExtension(fileExtension))
123 | {
124 | throw new ArgumentOutOfRangeException($"The file extension of the image \"{Path.GetFileName(filePath)}\" " +
125 | $"is not supported by Steam.");
126 | }
127 |
128 | string toPath = Path.Combine(gridDirectoryPath, fileNameWithoutExtension + fileExtension);
129 |
130 | try
131 | {
132 | HardLink.Create(toPath, filePath);
133 | }
134 | catch (Win32Exception hardLinkEx)
135 | {
136 | logger.Warn($"Could not create a hard link: \"{hardLinkEx.Message}\", copying file instead.");
137 | try
138 | {
139 | File.Copy(filePath, toPath, true);
140 | }
141 | catch (Exception ex) // TODO: Too general?
142 | {
143 | logger.Error(ex, $"Failed to copy image file, no Steam shortcut image was added.");
144 | return;
145 | }
146 | }
147 |
148 | currentImagePath = toPath;
149 | }
150 |
151 | private string GetExistingAssetPath()
152 | {
153 | // Assuming that there is not, for some reason,
154 | // multiple files for the same asset with different file extensions.
155 | return Directory.GetFiles(gridDirectoryPath).FirstOrDefault(
156 | filePath => Path.GetFileName(filePath)
157 | .StartsWith(fileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase) &&
158 | HasValidFileExtension(Path.GetExtension(filePath)));
159 | }
160 |
161 | ///
162 | /// Checks if the file extension is supported by Steam.
163 | /// Supported types: .png (including animated ones), .jpg and .jpeg.
164 | ///
165 | /// The file extension (including the initial dot).
166 | /// true if valid; false otherwise.
167 | public static bool HasValidFileExtension(string fileExtension)
168 | {
169 | return !string.IsNullOrEmpty(fileExtension) && SupportedFileExtensions.Any(
170 | ext => ext.Equals(fileExtension, StringComparison.OrdinalIgnoreCase));
171 | }
172 |
173 | private static string GetGridDirectoryPath()
174 | {
175 | string dirPath = Path.Combine(
176 | Path.GetDirectoryName(GlosSIIntegration.GetSettings().SteamShortcutsPath), "grid");
177 | if (!Directory.Exists(dirPath))
178 | {
179 | Directory.CreateDirectory(dirPath);
180 | }
181 |
182 | return dirPath;
183 | }
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/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/SteamLauncher/Steam.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Win32;
2 | using System;
3 | using System.IO;
4 |
5 | namespace GlosSIIntegration.Models.SteamLauncher
6 | {
7 | internal static class Steam
8 | {
9 | private const string SteamRegistryPath = @"HKEY_CURRENT_USER\SOFTWARE\Valve\Steam";
10 | private const string SteamActiveProcessRegistryPath = SteamRegistryPath + @"\ActiveProcess";
11 | private static string steamLanguage = null;
12 | private static string bpmWindowTitle;
13 | private static string desktopWindowTitle;
14 | private static string desktopMiniWindowTitle;
15 |
16 | ///
17 | /// The path to the Steam directory. For example: c:/program files (x86)/steam
18 | ///
19 | public static string Path { get; }
20 |
21 | ///
22 | /// The current mode Steam is in.
23 | /// null if Steam proper is not currently running.
24 | ///
25 | public static ISteamMode Mode
26 | {
27 | get
28 | {
29 | WinWindow window;
30 | ValidateWindowTitles();
31 |
32 | window = FindDesktopModeWindow();
33 | if (window != null) return new SteamDesktopMode(window);
34 | window = FindSteamWindow(bpmWindowTitle);
35 | if (window != null) return new SteamBigPictureMode(window);
36 |
37 | return null;
38 | }
39 | }
40 |
41 | ///
42 | /// The id of the currently active Steam user, or 0 if no user is currently active.
43 | ///
44 | public static uint ActiveUser
45 | {
46 | get
47 | {
48 | object registryValue = Registry.GetValue(SteamActiveProcessRegistryPath, "ActiveUser", null)
49 | ?? throw new FormatException("ActiveUser not found.");
50 | return (uint)(int)registryValue;
51 | }
52 | }
53 |
54 | static Steam()
55 | {
56 | // TODO: Handle exception? Should suffice to verify that Steam is installed in the verification code?
57 | Path = ReadSteamRegistryString("SteamPath");
58 | ValidateWindowTitles();
59 | }
60 |
61 | ///
62 | /// Validates the window titles. These can differ depending on which localized strings are used.
63 | ///
64 | private static void ValidateWindowTitles()
65 | {
66 | string curSteamLanguage = ReadSteamRegistryString("Language");
67 | if (steamLanguage != curSteamLanguage)
68 | {
69 | steamLanguage = curSteamLanguage;
70 | string localizations = ReadSteamUILocalizations();
71 | desktopWindowTitle = ReadSteamLocalizationString(localizations, "WindowName_SteamDesktop");
72 | desktopMiniWindowTitle = ReadSteamLocalizationString(localizations, "WindowName_SteamDesktopMini");
73 | bpmWindowTitle = ReadSteamLocalizationString(localizations, "SP_WindowTitle_BigPicture");
74 | }
75 | }
76 |
77 | ///
78 | /// Tries to find the running desktop mode window, if any.
79 | /// This is done without checking if the language of Steam (i.e. the searched for window titles) have changed.
80 | ///
81 | /// The found window, or null if no window was found.
82 | internal static WinWindow FindDesktopModeWindow()
83 | {
84 | return FindSteamWindow(desktopWindowTitle) ?? FindSteamWindow(desktopMiniWindowTitle);
85 | }
86 |
87 | ///
88 | /// Finds a Steam window with the supplied window title.
89 | ///
90 | /// The window title.
91 | /// The found window, or null if no window was found.
92 | private static WinWindow FindSteamWindow(string windowTitle)
93 | {
94 | return WinWindow.Find("SDL_app", windowTitle);
95 | }
96 |
97 | private static string ReadSteamRegistryString(string valueName)
98 | {
99 | return Registry.GetValue(SteamRegistryPath, valueName, null) as string
100 | ?? throw new FormatException($"\"{valueName}\" Steam registry string not found.");
101 | }
102 |
103 | private static string ReadSteamUILocalizations()
104 | {
105 | return File.ReadAllText($"{Path}/steamui/localization/steamui_{steamLanguage}-json.js");
106 | }
107 |
108 | // Simple key value reader.
109 | private static string ReadSteamLocalizationString(string localizations, string key)
110 | {
111 | key = "\"" + key + "\":\"";
112 |
113 | int titleKeyStartIndex = localizations.LastIndexOf(key, StringComparison.Ordinal);
114 | int titleKeyLength = key.Length;
115 | if (titleKeyStartIndex == -1)
116 | {
117 | throw new FileFormatException($"The {key} JSON key " +
118 | "could not be found in the Steam localization file.");
119 | }
120 |
121 | int titleValueStartIndex = titleKeyStartIndex + titleKeyLength;
122 | // Assuming that this is not the last option, since the value includes ,".
123 | // This is the case for all currently searched for localized strings.
124 | int titleValueEndIndex = localizations.IndexOf(@""",""", titleValueStartIndex, StringComparison.Ordinal);
125 | if (titleValueEndIndex == -1)
126 | {
127 | throw new FileFormatException($"The end of the {key} JSON value " +
128 | "could not be found in the Steam localization file.");
129 | }
130 | int titleValueLength = titleValueEndIndex - titleKeyStartIndex - titleKeyLength;
131 |
132 | return localizations.Substring(titleValueStartIndex, titleValueLength);
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/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/Models/SteamLauncher/SteamDesktopMode.cs:
--------------------------------------------------------------------------------
1 | using Playnite.SDK;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Text.RegularExpressions;
6 | using System.Threading.Tasks;
7 |
8 | namespace GlosSIIntegration.Models.SteamLauncher
9 | {
10 | internal class SteamDesktopMode : ISteamMode
11 | {
12 | private static readonly ILogger logger = LogManager.GetLogger();
13 | public WinWindow MainWindow { get; }
14 |
15 | internal SteamDesktopMode(WinWindow mainWindow)
16 | {
17 | MainWindow = mainWindow;
18 | }
19 |
20 | ///
21 | /// Checks if Steam is configured to occasionally notify the user about available games (marketing messages).
22 | ///
23 | /// If the value of the option could not be found.
24 | /// true if Steam is configured to notify the user; false if disabled.
25 | private static async Task IsSteamNotifyAvailableGamesEnabled()
26 | {
27 | string configFilePath = Path.Combine(Steam.Path, "userdata", Steam.ActiveUser.ToString(), @"config\localconfig.vdf");
28 | Regex rgx = new Regex(@"(?<=^\s*""NotifyAvailableGames""\s*"")[01](?="")");
29 |
30 | using (StreamReader reader = File.OpenText(configFilePath))
31 | {
32 | string line;
33 | while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
34 | {
35 | Match match = rgx.Match(line);
36 | if (match.Success)
37 | {
38 | return match.Value != "0";
39 | }
40 | }
41 | }
42 |
43 | throw new FileFormatException("Could not find whether Steam notifies available games.");
44 | }
45 |
46 | ///
47 | /// Waits for the Steam desktop window to start.
48 | ///
49 | /// If Steam Desktop mode did not start in time.
50 | /// The Steam desktop window.
51 | private static async Task WaitForSteamDesktopWindowToStart()
52 | {
53 | int msTimeout = 10000;
54 | const int msPollingInterval = 20;
55 | WinWindow desktopWindow;
56 |
57 | do
58 | {
59 | if ((desktopWindow = Steam.FindDesktopModeWindow()) != null)
60 | {
61 | return desktopWindow;
62 | }
63 | await Task.Delay(msPollingInterval).ConfigureAwait(false);
64 | } while ((msTimeout -= msPollingInterval) >= 0);
65 |
66 | throw new TimeoutException("Steam desktop mode did not start in time.");
67 | }
68 |
69 | ///
70 | /// Steam windows we do not want to see.
71 | /// Add a window to disable input and transition animations for the window
72 | /// as well as minimize it.
73 | /// Dispose the object to undo all changes.
74 | ///
75 | private class UnwantedSteamWindows : IDisposable
76 | {
77 | private readonly List steamWindows;
78 |
79 | public UnwantedSteamWindows()
80 | {
81 | steamWindows = new List();
82 | }
83 |
84 | public void Add(WinWindow window)
85 | {
86 | if (!steamWindows.Contains(window))
87 | {
88 | steamWindows.Add(window);
89 | window.DisableTransitionsAnimations();
90 | // Not really necessary, but could protect against misclicks.
91 | window.DisableInput();
92 | }
93 | window.Minimize();
94 | }
95 |
96 | public void Dispose()
97 | {
98 | foreach (WinWindow window in steamWindows)
99 | {
100 | window.EnableInput();
101 | window.EnableTransitionsAnimations();
102 | }
103 | }
104 | }
105 |
106 | // TODO: Increase delays when polling where reasonable.
107 | ///
108 | /// Attempts to return Steam to desktop mode (from Big Picture mode) without showing Steam desktop mode.
109 | /// Note: This method focuses the currently focused window afterwards.
110 | /// Try to ensure that the user is not busy interacting with windows when this method is called.
111 | /// This method is far from perfect, as it heavily relies on polling and timings.
112 | /// But there is not much of a choice (aside from hooking method calls).
113 | ///
114 | /// The currently active Steam Big Picture mode.
115 | /// If the Steam desktop window did not appear in time.
116 | public static async Task StealthilyReturnSteamToDesktopMode(SteamBigPictureMode bpm)
117 | {
118 | // When we exit Steam Big Picture mode, Steam will open the desktop mode window.
119 | // This is done presumably using SDL's RaiseWindow method, which will forcibly take focus using AttachThreadInput().
120 | // Focus is forcibly taken twice for the main window.
121 | // But Steam may also show marketing messages (notably the "Special Offers" window).
122 | // And if this window is shown (~3 seconds after the second forced focusing), it will also take forcibly take focus.
123 | // The process to combat this is therefore quite slow and unreliable.
124 |
125 | WinWindow thisWindow = WinWindow.GetFocusedWindow();
126 | const int msPollingInterval = 20;
127 | int msTimeout = 3000;
128 | int timesForegroundWindow = 0;
129 | bool steamMightShowMarketingMessage;
130 |
131 | bpm.Exit();
132 |
133 | try
134 | {
135 | steamMightShowMarketingMessage = await IsSteamNotifyAvailableGamesEnabled().ConfigureAwait(false);
136 | }
137 | catch (FileFormatException)
138 | {
139 | steamMightShowMarketingMessage = true;
140 | }
141 |
142 | WinWindow steamDesktopWindow = await WaitForSteamDesktopWindowToStart().ConfigureAwait(false);
143 |
144 | using (UnwantedSteamWindows unwantedSteamWindows = new UnwantedSteamWindows())
145 | {
146 | unwantedSteamWindows.Add(steamDesktopWindow);
147 | do
148 | {
149 | WinWindow focusedWindow = WinWindow.GetFocusedWindow();
150 |
151 | if (!Equals(thisWindow, focusedWindow))
152 | {
153 | if (focusedWindow == null)
154 | {
155 | // Should not really happen.
156 | logger.Warn("HWND 0 was focused when exiting Steam Big Picture mode.");
157 | }
158 | else if (focusedWindow.Equals(steamDesktopWindow))
159 | {
160 | steamDesktopWindow.Minimize();
161 | timesForegroundWindow++;
162 | msTimeout = 3000; // Reset timeout: the next focus theft should come soon.
163 | }
164 | else if (focusedWindow.GetProcessId() == steamDesktopWindow.GetProcessId())
165 | {
166 | // The focused window is a Steam window that is not the main desktop window:
167 | // it is most likely a marketing message.
168 | unwantedSteamWindows.Add(focusedWindow);
169 | focusedWindow.Minimize();
170 | timesForegroundWindow++;
171 | msTimeout = 3000; // Reset timeout.
172 | }
173 | else
174 | {
175 | // Some other non-Steam window was focused. Let's stop our shenanigans.
176 | logger.Warn("Non-Steam window was focused when exiting Steam Big Picture mode.");
177 | return;
178 | }
179 |
180 | thisWindow.Focus(); // Return focus. Should succeed provided that the currently focused window is 0.
181 |
182 | if (timesForegroundWindow == 2 && !steamMightShowMarketingMessage)
183 | {
184 | // No annoying notification should appear! We are done.
185 | logger.Trace("Steam was focused two times and marketing messages are disabled.");
186 | return;
187 | }
188 | else if (timesForegroundWindow == 3)
189 | {
190 | // We are most probably done now.
191 | logger.Trace("Steam was focused three times.");
192 | return;
193 | }
194 | }
195 |
196 | await Task.Delay(msPollingInterval).ConfigureAwait(false);
197 | } while ((msTimeout -= msPollingInterval) >= 0);
198 | }
199 |
200 | if (msTimeout < 0)
201 | {
202 | if (timesForegroundWindow == 0)
203 | {
204 | throw new TimeoutException("Waiting for Steam desktop mode to steal focus timed out!");
205 | }
206 |
207 | // We are forced to rely on a timeout.
208 | logger.Trace("Waiting for Steam focus theft timed out.");
209 | if (timesForegroundWindow == 1)
210 | {
211 | logger.Warn("Missed the second Steam desktop mode focus theft.");
212 | }
213 | }
214 | }
215 | }
216 | }
--------------------------------------------------------------------------------
/Source/Models/WinWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Runtime.InteropServices;
4 |
5 | namespace GlosSIIntegration.Models
6 | {
7 | ///
8 | /// Represents a Windows window with some useful operations. Note that the window can be closed at any time.
9 | ///
10 | internal class WinWindow
11 | {
12 | #region Win32
13 | [DllImport("User32.dll", CharSet = CharSet.Unicode)]
14 | private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
15 |
16 | [DllImport("User32.dll", CharSet = CharSet.Unicode)]
17 | private static extern IntPtr FindWindow(IntPtr intPtrZero, string lpWindowName);
18 |
19 | [DllImport("User32.dll")]
20 | private static extern IntPtr GetForegroundWindow();
21 |
22 | [DllImport("User32.dll")]
23 | private static extern bool SetForegroundWindow(IntPtr hWnd);
24 |
25 | [DllImport("User32.dll", SetLastError = true)]
26 | private static extern bool LockSetForegroundWindow(SetForegroundWindowLock lockState);
27 |
28 | private enum SetForegroundWindowLock : int
29 | {
30 | Lock = 1,
31 | Unlock = 2
32 | }
33 |
34 | [DllImport("User32.dll", SetLastError = true)]
35 | private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
36 |
37 | [DllImport("User32.dll", SetLastError = true)]
38 | private static extern bool PostMessage(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
39 |
40 | private enum WindowMessage : uint
41 | {
42 | Close = 0x0010
43 | }
44 |
45 | [DllImport("User32.dll")]
46 | private static extern bool EnableWindow(IntPtr hWnd, bool enable);
47 |
48 | [DllImport("User32.dll")]
49 | private static extern bool ShowWindow(IntPtr hWnd, ShowWindowOption nCmdShow);
50 |
51 | ///
52 | /// Controls how a window is to be shown when passed to
53 | ///
54 | private enum ShowWindowOption : int
55 | {
56 | ///
57 | /// Hides the window and activates another window.
58 | ///
59 | Hide,
60 | ShowNormal,
61 | Normal = 1,
62 | ShowMinimized,
63 | ShowMaximized,
64 | Maximize = 3,
65 | ShowNoActivate,
66 | Show,
67 | Minimize,
68 | ShowMinNoActive,
69 | ShowNA,
70 | Restore,
71 | ShowDefault,
72 | ForceMinimize
73 | }
74 |
75 | [DllImport("Dwmapi.dll", PreserveSig = false)]
76 | private static extern void DwmSetWindowAttribute(IntPtr hWnd, DwmWindowAttribute dwAttribute, ref int pvAttribute, int cbAttribute);
77 |
78 | private enum DwmWindowAttribute : uint
79 | {
80 | NcRendering_Enabled = 1,
81 | NcRendering_Policy,
82 | Transitions_ForceDisabled,
83 | Allow_NcPaint,
84 | Caption_Button_Bounds,
85 | Nonclient_Rtl_Layout,
86 | Force_Iconic_Representation,
87 | Flip3D_Policy,
88 | Extended_Frame_Bounds,
89 | Has_Iconic_Bitmap,
90 | Disallow_Peek,
91 | Excluded_From_Peek,
92 | Cloak,
93 | Cloaked,
94 | Freeze_Representation,
95 | Passive_Update_Mode,
96 | Use_HostBackdropBrush,
97 | Use_Immersive_Dark_Mode = 20,
98 | Window_Corner_Preference = 33,
99 | Border_Color,
100 | Caption_Color,
101 | Text_Color,
102 | Visible_Frame_Border_Thickness,
103 | SystemBackdrop_Type,
104 | Last
105 | };
106 | #endregion Win32
107 |
108 | protected readonly IntPtr handle;
109 |
110 | protected WinWindow(IntPtr hWnd)
111 | {
112 | handle = hWnd;
113 | }
114 |
115 | ///
116 | /// Finds a window by its name and window class.
117 | /// See
118 | /// Win32 FindWindow.
119 | ///
120 | /// The name of the window's window class.
121 | /// The name of the window.
122 | /// The window, or null if it fails.
123 | public static WinWindow Find(string windowClassName, string windowName)
124 | {
125 | return TryInstantiate(FindWindow(windowClassName, windowName));
126 | }
127 |
128 | public static WinWindow GetFocusedWindow()
129 | {
130 | return TryInstantiate(GetForegroundWindow());
131 | }
132 |
133 | ///
134 | /// Finds a window by its name.
135 | /// See
136 | /// Win32 FindWindow.
137 | ///
138 | /// The name of the window.
139 | /// The window, or null if it fails.
140 | public static WinWindow Find(string windowName)
141 | {
142 | return TryInstantiate(FindWindow(IntPtr.Zero, windowName));
143 | }
144 |
145 | ///
146 | /// Instantiates a from a window handle,
147 | /// provided that the handle is not .
148 | ///
149 | /// The window handle of the WinWindow to be instantiated.
150 | /// The instantiated ,
151 | /// or null if == .
152 | private static WinWindow TryInstantiate(IntPtr hWnd)
153 | {
154 | if (hWnd == IntPtr.Zero)
155 | {
156 | return null;
157 | }
158 |
159 | return new WinWindow(hWnd);
160 | }
161 |
162 | public static bool LockSetForegroundWindow()
163 | {
164 | return LockSetForegroundWindow(SetForegroundWindowLock.Lock);
165 | }
166 |
167 | public static void UnlockSetForegroundWindow()
168 | {
169 | LockSetForegroundWindow(SetForegroundWindowLock.Unlock);
170 | }
171 |
172 | ///
173 | /// Enables user input to the window.
174 | ///
175 | public void EnableInput()
176 | {
177 | EnableWindow(handle, true);
178 | }
179 |
180 | ///
181 | /// Disables user input to the window.
182 | ///
183 | public void DisableInput()
184 | {
185 | EnableWindow(handle, false);
186 | }
187 |
188 | ///
189 | /// Enables window transitions animations.
190 | ///
191 | public void EnableTransitionsAnimations()
192 | {
193 | SetTransitionsAnimations(0);
194 | }
195 |
196 | ///
197 | /// Disables window transitions animations.
198 | ///
199 | public void DisableTransitionsAnimations()
200 | {
201 | SetTransitionsAnimations(1);
202 | }
203 |
204 | ///
205 | /// Sets window transitions animations on or off.
206 | ///
207 | /// true to disable animations; false otherwise.
208 | private void SetTransitionsAnimations(int disable)
209 | {
210 | DwmSetWindowAttribute(handle, DwmWindowAttribute.Transitions_ForceDisabled, ref disable, sizeof(int));
211 | }
212 |
213 | public override bool Equals(object obj)
214 | {
215 | return obj is WinWindow otherWindow && handle == otherWindow.handle;
216 | }
217 |
218 | public override int GetHashCode()
219 | {
220 | return handle.ToInt32();
221 | }
222 |
223 | public uint GetProcessId()
224 | {
225 | return GetWindowThreadProcessId(handle, out uint pid) == 0 ? throw new Win32Exception() : pid;
226 | }
227 |
228 | ///
229 | /// Checks if the window is the foreground window.
230 | ///
231 | /// true if this window is the foreground window; false otherwise.
232 | public bool IsFocused()
233 | {
234 | return handle == GetForegroundWindow();
235 | }
236 |
237 | ///
238 | /// Tells the window to please close.
239 | ///
240 | /// If closing the window failed,
241 | /// for example if the window has already been closed.
242 | public void Close()
243 | {
244 | if (!PostMessage(handle, WindowMessage.Close, IntPtr.Zero, IntPtr.Zero))
245 | {
246 | throw new InvalidOperationException("Failed to close the window", new Win32Exception());
247 | }
248 | }
249 |
250 | ///
251 | /// Tries to focus the window.
252 | ///
253 | /// true if the window was focused; false otherwise.
254 | public bool Focus()
255 | {
256 | return SetForegroundWindow(handle);
257 | }
258 |
259 | ///
260 | /// Minimizes the window without activating it.
261 | ///
262 | public void Minimize()
263 | {
264 | ShowWindow(handle, ShowWindowOption.ShowMinNoActive);
265 | }
266 |
267 | ///
268 | /// Shows the window.
269 | ///
270 | public void Show()
271 | {
272 | ShowWindow(handle, ShowWindowOption.Show);
273 | }
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/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/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/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 | ..\Resources\DefaultSteamShortcutIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
123 |
124 |
125 | ..\Resources\DefaultTarget.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
126 |
127 |
--------------------------------------------------------------------------------
/Source/Resources/DefaultSteamShortcutIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Source/Resources/DefaultSteamShortcutIcon.png
--------------------------------------------------------------------------------
/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/Scripts/StartPlayniteFromGlosSI.vbs:
--------------------------------------------------------------------------------
1 | ' Script that runs Playnite, and potentially a Playnite game with focus,
2 | ' and prevents Steam from waiting on Playnite to close (due to misidentifying it as a game process).
3 | ' This script is intended to be run by GlosSITarget when started via Steam.
4 | ' If a game should be started, the Playnite database ID of the game must be provided as the argument.
5 | ' Otherwise only Playnite will be started.
6 | ' Note that multiple installations of Playnite is currently not supported.
7 | ' This is also the case for Playnite's own desktop shortcuts.
8 |
9 | const strGeneralPlayniteArguments = "--nolibupdate --hidesplashscreen"
10 | Set objWMIService = GetObject("winmgmts:")
11 | Set objWSShell = CreateObject("WScript.Shell")
12 |
13 | ' Focus Playnite if it is already running. Ensures that the started game becomes focused.
14 | FocusPlaynite()
15 |
16 | ' Get the Playnite start arguments.
17 | If wscript.arguments.count > 0 Then
18 | strStartArguments = "--start " & WScript.Arguments(0)
19 | ElseIf IsSteamBigPictureModeRunning() Then
20 | strStartArguments = "--startfullscreen"
21 | Else
22 | strStartArguments = "--startdesktop"
23 | End If
24 |
25 | StartPlaynite(strGeneralPlayniteArguments & " " & strStartArguments)
26 |
27 | ' Start Playnite with the provided application arguments.
28 | Function StartPlaynite(strArguments)
29 | ' Configure the Playnite process.
30 | Set objStartup = objWMIService.Get("Win32_ProcessStartup")
31 | Set objConfig = objStartup.SpawnInstance_
32 | objConfig.ShowWindow = SW_NORMAL
33 | Set objProcess = objWMIService.Get("Win32_Process")
34 |
35 | ' Start Playnite. By using the Create method, Steam will consider the started process as unrelated to GlosSI.
36 | intResult = objProcess.Create(GetPlayniteAppPath() & " " & strArguments, null, objConfig, intProcessId)
37 |
38 | If intResult Then
39 | Call MsgBox("Starting Playnite failed: error code " & intResult & ".", 16, _
40 | "Playnite GlosSI Integration Extension")
41 | End If
42 | End Function
43 |
44 | ' Focuses Playnite if it is running.
45 | Function FocusPlaynite()
46 | Set colPlayniteProcess = objWMIService.ExecQuery("Select * From Win32_Process Where " & _
47 | "name = 'Playnite.DesktopApp.exe' Or name = 'Playnite.FullscreenApp.exe'")
48 | If colPlayniteProcess.Count Then
49 | objWSShell.AppActivate(colPlayniteProcess.ItemIndex(0).ProcessID)
50 | End If
51 | End Function
52 |
53 |
54 | ' Gets the path to the Playnite executable (with quotation marks).
55 | Function GetPlayniteAppPath()
56 | ' Read the registry to find the path to Playnite.
57 | strRegData = objWSShell.RegRead("HKEY_CLASSES_ROOT\Playnite\shell\open\command\")
58 | GetPlayniteAppPath = Mid(strRegData, 1, Len(strRegData) - Len(" --uridata ""%1"""))
59 | End Function
60 |
61 | ' Gets the localized window title of the Steam Big Picture mode window.
62 | Function GetSteamBigPictureModeWindowTitle()
63 | ' Get the path to the Steam directory.
64 | strSteamPath = objWSShell.RegRead("HKEY_CURRENT_USER\SOFTWARE\Valve\Steam\SteamPath")
65 | ' Get the Steam language.
66 | strSteamLanguage = objWSShell.RegRead("HKEY_CURRENT_USER\SOFTWARE\Valve\Steam\Language")
67 |
68 | ' Read the Steam localization file that contains the window title.
69 | Set objFSO = CreateObject("Scripting.FileSystemObject")
70 | Set objFile = objFSO.OpenTextFile(strSteamPath & "/steamui/localization/steamui_" & _
71 | strSteamLanguage & "-json.js", 1)
72 | strFileContents = objFile.ReadAll()
73 | objFile.Close
74 |
75 | ' Extract the window title.
76 | ' Example string: "SP_WindowTitle_BigPicture":"Steam: Big Picture-modus"
77 | const strTarget = """SP_WindowTitle_BigPicture"":"""
78 | intTargetLen = Len(strTarget)
79 | lngTargetStartPos = InStrRev(strFileContents, strTarget)
80 | lngTargetEndPos = InStr(lngTargetStartPos + intTargetLen, strFileContents, """,""")
81 | GetSteamBigPictureModeWindowTitle = Mid(strFileContents, lngTargetStartPos + intTargetLen, _
82 | lngTargetEndPos - lngTargetStartPos - intTargetLen)
83 | End Function
84 |
85 | ' Checks if Steam is currently in Big Picture mode.
86 | Function IsSteamBigPictureModeRunning()
87 | ' Since Win32 FindWindow() and EnumWindows() cannot be called in VBScript, TASKLIST is used instead.
88 | ' It should currently not be necessary to escape the Steam window title.
89 | IsSteamBigPictureModeRunning = objWSShell.Run("CMD /V /C """ & _
90 | "TASKLIST /FI ""IMAGENAME eq steamwebhelper.exe"" /FI ""WINDOWTITLE eq " & _
91 | GetSteamBigPictureModeWindowTitle() & """ /FO CSV /NH | " & _
92 | "FIND """""""" & " & _
93 | "EXIT /B !ERRORLEVEL!""", 0, True) = 0
94 | End Function
--------------------------------------------------------------------------------
/Source/Views/GlosSIIntegrationSettingsView.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/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/Views/ShortcutCreationView.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/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/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/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LemmusLemmus/GlosSI-Integration-Playnite/6fb30630459271350c714ddaed41173edb1f29de/Source/icon.png
--------------------------------------------------------------------------------
/Source/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | files:
2 | - source: /Source/Localization/loc_source.xaml
3 | translation: /Source/Localization/%locale_with_underscore%.xaml
--------------------------------------------------------------------------------