Skip to content

Commit c87d114

Browse files
committed
Unpackaged support
1 parent 741e88e commit c87d114

File tree

16 files changed

+474
-106
lines changed

16 files changed

+474
-106
lines changed

Amethyst.Plugins.Contract/Classes.cs

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Data;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Numerics;
4+
using System.Reflection;
45
using System.Runtime.Serialization;
56
using System.Text.Json.Serialization;
67

@@ -316,48 +317,47 @@ public interface IDependency
316317
public Task<bool> Install(IProgress<InstallationProgress> progress, CancellationToken cancellationToken);
317318
}
318319

319-
320320
// Fix applier worker helper class
321321
public interface IFix
322322
{
323-
/// <summary>
324-
/// [Title]
325-
/// The name of the fix to be applied
326-
/// </summary>
327-
public string Name { get; }
328-
329-
/// <summary>
330-
/// Whether it is a must to have this fix applied
331-
/// (IsNecessary=true) for the plugin to be working properly
332-
/// </summary>
333-
public bool IsMandatory { get; }
334-
335-
/// <summary>
336-
/// Check whether the fix is (already) applied
337-
/// </summary>
338-
public bool IsNecessary { get; }
339-
340-
/// <summary>
341-
/// If there is a need to accept an EULA, pass its contents here
342-
/// </summary>
343-
public string InstallerEula { get; }
344-
345-
/// <summary>
346-
/// Perform the main installation action, reporting the progress
347-
/// </summary>
348-
/// <param name="progress">
349-
/// Report the progress using InstallationProgress
350-
/// </param>
351-
/// <param name="cancellationToken">
352-
/// CancellationToken for task cancellation
353-
/// </param>
354-
/// <param name="arg">
355-
/// Optional argument
356-
/// </param>
357-
/// <returns>
358-
/// Success?
359-
/// </returns>
360-
public Task<bool> Apply(IProgress<InstallationProgress> progress, CancellationToken cancellationToken, object? arg = null);
323+
/// <summary>
324+
/// [Title]
325+
/// The name of the fix to be applied
326+
/// </summary>
327+
public string Name { get; }
328+
329+
/// <summary>
330+
/// Whether it is a must to have this fix applied
331+
/// (IsNecessary=true) for the plugin to be working properly
332+
/// </summary>
333+
public bool IsMandatory { get; }
334+
335+
/// <summary>
336+
/// Check whether the fix is (already) applied
337+
/// </summary>
338+
public bool IsNecessary { get; }
339+
340+
/// <summary>
341+
/// If there is a need to accept an EULA, pass its contents here
342+
/// </summary>
343+
public string InstallerEula { get; }
344+
345+
/// <summary>
346+
/// Perform the main installation action, reporting the progress
347+
/// </summary>
348+
/// <param name="progress">
349+
/// Report the progress using InstallationProgress
350+
/// </param>
351+
/// <param name="cancellationToken">
352+
/// CancellationToken for task cancellation
353+
/// </param>
354+
/// <param name="arg">
355+
/// Optional argument
356+
/// </param>
357+
/// <returns>
358+
/// Success?
359+
/// </returns>
360+
public Task<bool> Apply(IProgress<InstallationProgress> progress, CancellationToken cancellationToken, object? arg = null);
361361
}
362362

363363
// Plugin settings helper class
@@ -368,4 +368,38 @@ public interface IPluginSettings
368368

369369
// Write a serialized object to the plugin settings
370370
public void SetSetting<T>(object key, T? value);
371+
}
372+
373+
// Path helper class
374+
public interface IPathHelper
375+
{
376+
// Main Amethyst.exe location
377+
public FileInfo ProgramLocation { get; }
378+
379+
// AppData/TempState
380+
public DirectoryInfo TemporaryFolder { get; }
381+
382+
// AppData/LocalState
383+
public DirectoryInfo LocalFolder { get; }
384+
385+
// The location of ALL plugins folder
386+
public Task<DirectoryInfo> GetPluginsFolder();
387+
388+
// The location of plugin working copies
389+
public Task<DirectoryInfo> GetPluginsTempFolder();
390+
391+
// Get file from AppData/LocalState
392+
public Task<FileInfo> GetAppDataFile(string relativeFilePath);
393+
394+
// Get folder from shared (not packed) plugin folder
395+
public Task<DirectoryInfo> GetAppDataPluginFolder(string relativeFilePath);
396+
397+
// Get folder from working copy plugin folder
398+
public Task<DirectoryInfo> GetTempPluginFolder(string relativeFilePath);
399+
400+
// Get file path from AppData/LocalState
401+
public FileInfo GetAppDataFilePath(string relativeFilePath);
402+
403+
// Get log file path from AppData/TempState
404+
public FileInfo GetAppDataLogFilePath(string relativeFilePath);
371405
}

Amethyst.Plugins.Contract/Contract.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,16 @@ public interface IAmethystHost
400400
/// </summary>
401401
public IPluginSettings PluginSettings { get; }
402402

403+
/// <summary>
404+
/// Helper class that provides path-related methods,
405+
/// as Amethyst may be both packaged and unpackaged,
406+
/// this aims to make path handling easier and safer.
407+
/// In packaged mode, AppData will return shared, packaged AppData
408+
/// and in unpackaged mode, AppData will be created near main exe.
409+
/// This is to ensure portability and prevent the need for cleanup
410+
/// </summary>
411+
public IPathHelper PathHelper { get; }
412+
403413
/// <summary>
404414
/// Get the hook joint pose (typically Head, fallback to .First())
405415
/// of the currently selected base tracking device (no overrides!)

Amethyst/Amethyst.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
5050
</PropertyGroup>
5151

52+
<PropertyGroup>
53+
<!-- Workaround for MSB3271 error on processor architecture mismatch -->
54+
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
55+
</PropertyGroup>
56+
5257
<ItemGroup>
5358
<PackageReference Include="CommunityToolkit.WinUI" Version="7.1.2" />
5459
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
@@ -62,7 +67,7 @@
6267
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.10.0" />
6368
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240627000" />
6469
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1" />
65-
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.8" />
70+
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
6671
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
6772
<PackageReference Include="RestSharp" Version="108.0.3" />
6873
<PackageReference Include="System.ComponentModel.Composition" Version="8.0.0" />

Amethyst/App.xaml.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@
3636
using Newtonsoft.Json;
3737
using Newtonsoft.Json.Linq;
3838
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
39-
using System.Text;
40-
41-
// To learn more about WinUI, the WinUI project structure,
42-
// and more about our project templates, see: http://aka.ms/winui-project-info.
4339

4440
namespace Amethyst;
4541

@@ -61,6 +57,7 @@ public partial class App : Application
6157
public App()
6258
{
6359
InitializeComponent();
60+
AsyncUtils.RunSync(PathsHandler.Setup);
6461

6562
// Listen for and log all uncaught second-chance exceptions : XamlApp
6663
UnhandledException += (_, e) =>
@@ -296,7 +293,7 @@ protected override async void OnLaunched(LaunchActivatedEventArgs eventArgs)
296293
ElementSoundPlayer.State = ElementSoundPlayerState.Off;
297294

298295
Logger.Info("Creating a new CrashWindow view...");
299-
_crashWindow = new CrashWindow(); // Create a new window
296+
_crashWindow = new CrashWindow(args); // Create a new window
300297

301298
Logger.Info($"Activating {_crashWindow.GetType()}...");
302299
_crashWindow.Activate(); // Activate the main window
@@ -328,8 +325,13 @@ protected override async void OnLaunched(LaunchActivatedEventArgs eventArgs)
328325
}
329326

330327
// Check if activated via uri
331-
var activationUri = (AppInstance.GetCurrent().GetActivatedEventArgs().Data as
332-
ProtocolActivatedEventArgs)?.Uri;
328+
var activationUri = PathsHandler.IsAmethystPackaged
329+
? (AppInstance.GetCurrent().GetActivatedEventArgs().Data as
330+
ProtocolActivatedEventArgs)?.Uri
331+
: args.Length > 1 && args[1].StartsWith("amethyst-app:") &&
332+
Uri.TryCreate(args[1], UriKind.RelativeOrAbsolute, out var au)
333+
? au
334+
: null;
333335

334336
// Check if there's any launch arguments
335337
if (activationUri is not null && activationUri.Segments.Length > 0)
@@ -683,7 +685,7 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
683685
if (!needToCreateNew)
684686
{
685687
Logger.Fatal(new AbandonedMutexException("Startup failed! The app is already running."));
686-
await "amethyst-app:crash-already-running".ToUri().LaunchAsync();
688+
await "amethyst-app:crash-already-running".Launch();
687689

688690
await Task.Delay(3000);
689691
Environment.Exit(0); // Exit peacefully
@@ -692,15 +694,15 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
692694
catch (Exception e)
693695
{
694696
Logger.Fatal(new AbandonedMutexException($"Startup failed! Mutex creation error: {e.Message}"));
695-
await "amethyst-app:crash-already-running".ToUri().LaunchAsync();
697+
await "amethyst-app:crash-already-running".Launch();
696698

697699
await Task.Delay(3000);
698700
Environment.Exit(0); // Exit peacefully
699701
}
700702

701703
Logger.Info("Starting the crash handler passing the app PID...");
702704
await ($"amethyst-app:crash-watchdog?pid={Environment.ProcessId}&log={Logger.LogFilePath}" +
703-
$"&crash={Interfacing.CrashFile.FullName}").ToUri().LaunchAsync();
705+
$"&crash={Interfacing.CrashFile.FullName}").Launch();
704706

705707
// Disable internal sounds
706708
ElementSoundPlayer.State = ElementSoundPlayerState.Off;
@@ -884,9 +886,9 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
884886
// Update 1.2.13.0: reset configuration
885887
try
886888
{
887-
if (ApplicationData.Current.LocalSettings.Values["SetupFinished"] as bool? ?? false)
889+
if (PathsHandler.LocalSettings["SetupFinished"] as bool? ?? false)
888890
{
889-
ApplicationData.Current.LocalSettings.Values.Remove("SetupFinished");
891+
PathsHandler.LocalSettings.Remove("SetupFinished");
890892
AppData.Settings = new AppSettings(); // Reset application settings
891893
AppData.Settings.SaveSettings(); // Save empty settings to file
892894

@@ -896,7 +898,7 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
896898
"a critical update, so you'll need to reconfigure it.");
897899

898900
// If there's an OVR driver folder, try to delete it if possible
899-
await (await ApplicationData.Current.LocalFolder.GetFolderAsync("Amethyst"))?.DeleteAsync();
901+
await (await PathsHandler.LocalFolder.GetFolderAsync("Amethyst"))?.DeleteAsync();
900902
}
901903
}
902904
catch (Exception ex)

Amethyst/Classes/AppData.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Windows.ApplicationModel;
22
using Amethyst.Utils;
3+
using System.Reflection;
34

45
namespace Amethyst.Classes;
56

@@ -10,7 +11,7 @@ public static class AppData
1011

1112
// Internal version number
1213
public static (string Display, string Internal)
13-
VersionString => (Package.Current.Id.Version.AsString(), "AZ_BUILD_NUMBER");
14+
VersionString => (Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0.0", "AZ_BUILD_NUMBER");
1415

1516
// Application settings
1617
public static AppSettings Settings { get; set; } = new();
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using Amethyst.Plugins.Contract;
8+
using Amethyst.Utils;
9+
using Newtonsoft.Json;
10+
using Windows.Storage;
11+
12+
namespace Amethyst.Classes;
13+
14+
public class AppDataContainer : INotifyPropertyChanged
15+
{
16+
// ReSharper disable once MemberCanBePrivate.Global
17+
public SortedDictionary<object, object> SettingsDictionary { get; set; } = new();
18+
19+
// MVVM stuff
20+
public event PropertyChangedEventHandler PropertyChanged;
21+
22+
// Save settings
23+
public void SaveSettings()
24+
{
25+
try
26+
{
27+
// Save host settings to $env:AppData/Amethyst/
28+
File.WriteAllText(Interfacing.GetAppDataFilePath("HostSettings.json"),
29+
JsonConvert.SerializeObject(this, Formatting.Indented));
30+
}
31+
catch (Exception e)
32+
{
33+
Logger.Error($"Error saving plugin settings! Message: {e.Message}");
34+
}
35+
}
36+
37+
// Re/Load settings
38+
public void ReadSettings()
39+
{
40+
try
41+
{
42+
// Read host settings from $env:AppData/Amethyst/
43+
SettingsDictionary = (JsonConvert.DeserializeObject<AppDataContainer>(File.ReadAllText(
44+
Interfacing.GetAppDataFilePath("HostSettings.json"))) ?? new AppDataContainer()).SettingsDictionary;
45+
}
46+
catch (Exception e)
47+
{
48+
Logger.Error($"Error reading host settings! Message: {e.Message}");
49+
SettingsDictionary = new SortedDictionary<object, object>(); // Reset if null
50+
}
51+
}
52+
53+
// Save settings
54+
public async Task SaveSettingsAsync(bool silent = false)
55+
{
56+
try
57+
{
58+
// Save host settings to $env:AppData/Amethyst/
59+
await File.WriteAllTextAsync(Interfacing.GetAppDataFilePath("HostSettings.json"),
60+
JsonConvert.SerializeObject(this, Formatting.Indented));
61+
}
62+
catch (Exception e)
63+
{
64+
if (!silent) Logger.Error($"Error saving host settings! Message: {e.Message}");
65+
}
66+
}
67+
68+
// Re/Load settings
69+
public async Task ReadSettingsAsync(bool silent = false)
70+
{
71+
try
72+
{
73+
// Read host settings from $env:AppData/Amethyst/
74+
SettingsDictionary = (JsonConvert.DeserializeObject<AppDataContainer>(await File.ReadAllTextAsync(
75+
Interfacing.GetAppDataFilePath("HostSettings.json"))) ?? new AppDataContainer()).SettingsDictionary;
76+
}
77+
catch (Exception e)
78+
{
79+
if (e is FileNotFoundException) await SaveSettingsAsync(silent);
80+
if (!silent) Logger.Error($"Error reading host settings! Message: {e.Message}");
81+
SettingsDictionary = new SortedDictionary<object, object>(); // Reset if null
82+
}
83+
}
84+
85+
public void OnPropertyChanged(string propName = null)
86+
{
87+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
88+
}
89+
90+
public object this[object key]
91+
{
92+
get => PathsHandler.IsAmethystPackaged
93+
? ApplicationData.Current.LocalSettings.Values[key?.ToString() ?? "INVALID"]
94+
: SettingsDictionary.GetValueOrDefault(key);
95+
set
96+
{
97+
if (PathsHandler.IsAmethystPackaged)
98+
SettingsDictionary[key] = value;
99+
else
100+
ApplicationData.Current.LocalSettings.Values[key?.ToString() ?? "INVALID"] = value;
101+
102+
SaveSettings();
103+
OnPropertyChanged(nameof(SettingsDictionary));
104+
}
105+
}
106+
107+
public void Remove(object key)
108+
{
109+
if (PathsHandler.IsAmethystPackaged)
110+
SettingsDictionary.Remove(key);
111+
else
112+
ApplicationData.Current.LocalSettings.Values.Remove(key?.ToString() ?? "INVALID");
113+
114+
SaveSettings();
115+
OnPropertyChanged(nameof(SettingsDictionary));
116+
}
117+
}

0 commit comments

Comments
 (0)