diff --git a/CringeBootstrap/Program.cs b/CringeBootstrap/Program.cs index a1a8ef9..c3fabbe 100644 --- a/CringeBootstrap/Program.cs +++ b/CringeBootstrap/Program.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.Loader; using CringeBootstrap; using CringeBootstrap.Abstractions; @@ -47,9 +48,20 @@ context.AddDependencyOverride("CringeLauncher"); context.AddDependencyOverride("CringePlugins"); context.AddDependencyOverride("EOSSDK"); -var launcher = context.LoadFromAssemblyName(new AssemblyName("CringeLauncher")); +var entrypoint = Environment.GetEnvironmentVariable("DOTNET_BOOTSTRAP_ENTRYPOINT") ?? + "CringeLauncher.Launcher, CringeLauncher"; +if (!TypeName.TryParse(entrypoint, out var entrypointName) || + entrypointName.AssemblyName is null) +{ + Console.Error.WriteLine($"Invalid entrypoint name: {entrypoint}"); + Console.Error.WriteLine("Bootstrap encountered a fatal error and will shutdown."); + Console.Read(); + return; +} -using var corePlugin = (ICorePlugin) launcher.CreateInstance("CringeLauncher.Launcher")!; +var launcher = context.LoadFromAssemblyName(entrypointName.AssemblyName.ToAssemblyName()); + +using var corePlugin = (ICorePlugin) launcher.CreateInstance(entrypointName.FullName)!; corePlugin.Initialize(args); corePlugin.Run(); \ No newline at end of file diff --git a/CringeBootstrap/Properties/launchSettings.json b/CringeBootstrap/Properties/launchSettings.json new file mode 100644 index 0000000..64e60c1 --- /dev/null +++ b/CringeBootstrap/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "CringeBootstrap": { + "commandName": "Project", + "commandLineArgs": "\"C:\\Program Files (x86)\\Steam\\steamapps\\common\\SpaceEngineers\\Bin64\\SpaceEngineers.exe\"", + "environmentVariables": { + "DOTNET_BOOTSTRAP_ENTRYPOINT": "CringeLauncher.UserDev.UserDevLauncher, CringeLauncher", + "DOTNET_USERDEV_RUNDIR": "data" + } + } + } +} diff --git a/CringeLauncher.slnx b/CringeLauncher.slnx index a5b95ea..3c7e623 100644 --- a/CringeLauncher.slnx +++ b/CringeLauncher.slnx @@ -1,16 +1,18 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CringeLauncher/Launcher.cs b/CringeLauncher/Launcher.cs index b5a3465..17fc9c5 100644 --- a/CringeLauncher/Launcher.cs +++ b/CringeLauncher/Launcher.cs @@ -45,15 +45,27 @@ namespace CringeLauncher; public class Launcher : ICorePlugin { - private const uint AppId = 244850U; + private readonly string? _gameDataDirectoryPathOverride; + protected const uint AppId = 244850U; private SpaceEngineersGame? _game; private readonly Harmony _harmony = new("CringeBootstrap"); private IPluginsLifetime? _lifetime; private MyGameRenderComponent? _renderComponent; - private readonly DirectoryInfo _configDir = Directory.CreateDirectory( - Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CringeLauncher", "config")); + private readonly DirectoryInfo _configDir; + private readonly DirectoryInfo _dir; + + public Launcher() : this(null) { } + + protected Launcher(string? gameDataDirectoryPathOverride) + { + _gameDataDirectoryPathOverride = gameDataDirectoryPathOverride; + _dir = Directory.CreateDirectory(Path.Join( + gameDataDirectoryPathOverride ?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "CringeLauncher")); + _configDir = _dir.CreateSubdirectory("config"); + } public void Initialize(string[] args) { @@ -107,7 +119,7 @@ public class Launcher : ICorePlugin MyFinalBuildConstants.APP_VERSION = MyPerGameSettings.BasicGameInfo.GameVersion.GetValueOrDefault(); MyShaderCompiler.Init(MyShaderCompiler.TargetPlatform.PC, false); MyVRageWindows.Init(MyPerGameSettings.BasicGameInfo.ApplicationName, MySandboxGame.Log, - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Path.Join(_gameDataDirectoryPathOverride ?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), MyPerGameSettings.BasicGameInfo.ApplicationName), false, false); @@ -168,7 +180,8 @@ public class Launcher : ICorePlugin var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError() .WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(1)); - services.AddHttpClient() + services.AddHttpClient((client, provider) => + new PluginsLifetime(provider.GetRequiredService(), client, _dir)) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All @@ -225,7 +238,7 @@ public class Launcher : ICorePlugin return MyVRage.Platform.Windows.Window; } - private async Task CheckUpdatesDisabledAsync(Logger logger) + protected virtual async Task CheckUpdatesDisabledAsync(Logger logger) { var path = Path.Join(_configDir.FullName, "launcher.json"); @@ -319,7 +332,7 @@ public class Launcher : ICorePlugin MyTexts.LoadTexts(textsPath, description.CultureName, description.SubcultureName); } - public static void InitUgc() + protected virtual void InitUgc() { var steamGameService = MySteamGameService.Create(false, AppId); MyServiceManager.Instance.AddService(steamGameService); diff --git a/CringeLauncher/UserDev/Networking/UserDevGameService.cs b/CringeLauncher/UserDev/Networking/UserDevGameService.cs new file mode 100644 index 0000000..444bc20 --- /dev/null +++ b/CringeLauncher/UserDev/Networking/UserDevGameService.cs @@ -0,0 +1,429 @@ +using VRage.GameServices; + +namespace CringeLauncher.UserDev.Networking; + +public class UserDevGameService(uint appId) : IMyGameService +{ + private readonly List m_packageIds = []; + + public uint AppId { get; } = appId; + + public bool IsActive => true; + + public bool IsOnline { get; } + + public bool IsOverlayEnabled { get; } + + public bool IsOverlayBrowserAvailable { get; } + + public bool OwnsGame { get; } + + public ulong UserId + { + get => 1234567891011; + set + { + } + } + + public ulong OnlineUserId => UserId; + + public char PlatformIcon => VRage.GameServices.PlatformIcon.PC; + + public string UserName => "Dev"; + + public string OnlineName => UserName; + + public bool CanQuickLaunch { get; } + + public void QuickLaunch() + { + } + + public MyGameServiceUniverse UserUniverse => MyGameServiceUniverse.Dev; + + public string BranchName => "userdev"; + + public string BranchNameFriendly => "UserDev"; + + public event Action? OnOverlayActivated; + + public event Action? OnDLCInstalled; + + public event Action? OnUserChanged; + + public event Action? OnSignInStateChanged; + + public event Action? OnActivityLaunch; + + public event Action? OnUpdate; + + public event Action? OnUpdateNetworkThread; + + public event Action? OnLobbyStart; + + public event Action? OnSessionUnload; + + public void OpenOverlayUrl(string url, bool predetermined = true) + { + } + + public void SetNotificationPosition(NotificationPosition notificationPosition) + { + } + + public void ShutDown() + { + } + + public bool IsAppInstalled(uint appId) => true; + + public void OpenDlcInShop(uint dlcId) + { + } + + public void OpenInventoryItemInShop(int itemId) + { + } + + public bool IsDlcSupported(uint dlcId) => true; + + public bool IsDlcInstalled(uint dlcId) => true; + + public void AddDlcPackages(List packages) + { + m_packageIds.Clear(); + foreach (var package in packages) + { + if (!string.IsNullOrEmpty(package.XboxPackageId) && !string.IsNullOrEmpty(package.XboxStoreId)) + m_packageIds.Add(new MyPackage + { + DlcId = package.AppId, + PackageId = package.XboxPackageId, + StoreId = package.XboxStoreId + }); + } + } + + public int GetDLCCount() => m_packageIds.Count; + + public bool GetDLCDataByIndex( + int index, + out uint dlcId, + out bool available, + out string name, + int nameBufferSize) + { + if (index >= m_packageIds.Count) + { + dlcId = 0U; + available = false; + name = string.Empty; + return false; + } + available = true; + dlcId = m_packageIds[index].DlcId; + name = "Dev" + index; + return true; + } + + public void OpenOverlayUser(ulong id) + { + } + + public bool GetAuthSessionTicket(out uint ticketHandle, byte[] buffer, out uint length) + { + ticketHandle = 0U; + length = 0U; + return false; + } + + public void LoadStats() + { + } + + public IMyAchievement? GetAchievement(string achievementName, string statName, float maxValue) + { + return null; + } + + public IMyAchievement? GetAchievement(string achievementName) => null; + + public void RegisterAchievement(string achievementName, string xblId) + { + } + + public void ResetAllStats(bool achievementsToo) + { + } + + public void StoreStats() + { + } + + public string GetPersonaName(ulong userId) => string.Empty; + + public bool HasFriend(ulong userId) => false; + + public string GetClanName(ulong groupId) => string.Empty; + + public void Update() + { + OnUpdate?.Invoke(); + } + + public void UpdateNetworkThread(bool sessionEnabled) + { + OnUpdateNetworkThread?.Invoke(sessionEnabled); + } + + public bool IsUserInGroup(ulong groupId) => false; + + public bool GetRemoteStorageQuota(out ulong totalBytes, out ulong availableBytes) + { + totalBytes = 0UL; + availableBytes = 0UL; + return false; + } + + public int GetRemoteStorageFileCount() => 0; + + public string GetRemoteStorageFileNameAndSize(int fileIndex, out int fileSizeInBytes) + { + fileSizeInBytes = 0; + return string.Empty; + } + + public bool IsRemoteStorageFilePersisted(string file) => false; + + public bool RemoteStorageFileForget(string file) => false; + + public ulong CreatePublishedFileUpdateRequest(ulong publishedFileId) => 0; + + public void UpdatePublishedFileTags(ulong updateHandle, string[] tags) + { + } + + public void UpdatePublishedFileFile(ulong updateHandle, string steamItemFileName) + { + } + + public void UpdatePublishedFilePreviewFile(ulong updateHandle, string steamPreviewFileName) + { + } + + public void FileDelete(string steamItemFileName) + { + } + + public bool FileExists(string fileName) => false; + + public int GetFileSize(string fileName) => 0; + + public ulong FileWriteStreamOpen(string fileName) => 0; + + public void FileWriteStreamWriteChunk(ulong handle, byte[] buffer, int size) + { + } + + public void FileWriteStreamClose(ulong handle) + { + } + + public void CommitPublishedFileUpdate( + ulong updateHandle, + Action onCallResult) + { + } + + public void PublishWorkshopFile( + string file, + string previewFile, + string title, + string description, + string longDescription, + MyPublishedFileVisibility visibility, + string[] tags, + Action onCallResult) + { + } + + public void SubscribePublishedFile( + ulong publishedFileId, + Action onCallResult) + { + } + + public void FileShare( + string file, + Action onCallResult) + { + } + + public string ServiceName => nameof (UserDevGameService); + + public string ServiceDisplayName => "UserDev Game Service"; + + public bool OpenProfileForMute { get; } + + public int GetFriendsCount() => 0; + + public ulong GetFriendIdByIndex(int index) => 0; + + public string GetFriendNameByIndex(int index) => string.Empty; + + public void SaveToCloudAsync( + string storageName, + byte[] buffer, + Action? completedAction) + { + completedAction?.Invoke(CloudResult.Failed); + } + + public CloudResult SaveToCloud(string fileName, byte[] buffer) => CloudResult.Failed; + + public CloudResult SaveToCloud(string containerName, List fileNames) + { + return CloudResult.Failed; + } + + public bool LoadFromCloudAsync(string fileName, Action completedAction) => false; + + public List? GetCloudFiles(string directoryFilter) + { + return null; + } + + public byte[]? LoadFromCloud(string fileName) => null; + + public bool DeleteFromCloud(string fileName) => false; + + public bool IsProductOwned(uint productId, out DateTime? purchaseTime) + { + purchaseTime = null; + return false; + } + + public void RequestEncryptedAppTicket(string url, Action onDone) + { + } + + public void RequestAuthToken(string clientId, Action onDone) + { + } + + public void RequestPermissions( + Permissions permission, + bool attemptResolution, + Action? onDone) + { + onDone?.Invoke(permission == Permissions.UGC ? PermissionResult.Error : PermissionResult.Granted); + } + + public void RequestPermissionsWithTargetUser( + Permissions permission, + ulong userId, + Action? onDone) + { + onDone?.Invoke(PermissionResult.Granted); + } + + public void OnThreadpoolInitialized() + { + } + + public bool GetInstallStatus(out int percentage) + { + percentage = 100; + return true; + } + + public void Trace(bool enable) + { + } + + public void SetPlayerMuted(ulong playerId, bool muted) + { + } + + public ulong[]? GetBlockListRaw() => null; + + HashSet? IMyGameService.GetBlockList() => null; + + public ulong[]? GetBlockList() => null; + + public bool IsPlayerMuted(ulong playerId) => false; + + public void UpdateMutedPlayers(Action onDone) => onDone.InvokeIfNotNull(); + + public MyGameServiceAccountType GetServerAccountType(ulong steamId) + { + return MyGameServiceAccountType.Invalid; + } + + public void DeleteUnnecessaryFilesFromTempFolder() + { + } + + public void OnSessionLoaded(string campaignName, string currentMissionName) + { + } + + public void OnSessionReady(bool multiplayer, bool dedicated) + { + } + + public void OnLoadingScreenCompleted() + { + } + + public void OnGameSaved(bool success, string savePath) + { + } + + public void OnCampaignFinishing() + { + } + + public void LobbyStarts() => OnLobbyStart.InvokeIfNotNull(); + + public void OnMissionFinished(string missionName) + { + } + + public void OnSessionUnloaded() => OnSessionUnload.InvokeIfNotNull(); + + public void OnSessionUnloading() + { + } + + public bool ActivityInProgress { get; } + + public LoadActivityResult GetActivityLoadInformation(string activityId) + { + return new LoadActivityResult(); + } + + public void OnPlayersChanged(int playersCount) + { + } + + public ulong GetModsCacheFreeSpace() => ulong.MaxValue; + + public void FormatModsCache() + { + } + + public void PrintStats() + { + } + + public bool IsPlayerBlocked(ulong playerId) => false; + + private struct MyPackage + { + public uint DlcId; + public string PackageId; + public string StoreId; + } +} \ No newline at end of file diff --git a/CringeLauncher/UserDev/UserDevLauncher.cs b/CringeLauncher/UserDev/UserDevLauncher.cs new file mode 100644 index 0000000..534020a --- /dev/null +++ b/CringeLauncher/UserDev/UserDevLauncher.cs @@ -0,0 +1,23 @@ +using CringeLauncher.UserDev.Networking; +using NLog; +using VRage; +using VRage.GameServices; + +namespace CringeLauncher.UserDev; + +public class UserDevLauncher() : Launcher(Environment.GetEnvironmentVariable("DOTNET_USERDEV_RUNDIR")) +{ + protected override void InitUgc() + { + var gameService = new UserDevGameService(AppId); + MyServiceManager.Instance.AddService(gameService); + MyServiceManager.Instance.AddService(new MyNullNetworking(gameService)); + MyServiceManager.Instance.AddService(new MyNullLobbyDiscovery()); + MyServiceManager.Instance.AddService(new MyNullServerDiscovery()); + } + + protected override Task CheckUpdatesDisabledAsync(Logger logger) + { + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/CringePlugins.MSBuild/CringePlugins.MSBuild.csproj b/CringePlugins.MSBuild/CringePlugins.MSBuild.csproj new file mode 100644 index 0000000..74f04fd --- /dev/null +++ b/CringePlugins.MSBuild/CringePlugins.MSBuild.csproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + latest + enable + + tasks + + NU5100 + embedded + true + 1.0.0 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + build\ + + + build\ + + + + diff --git a/CringePlugins.MSBuild/GenerateRunConfig.cs b/CringePlugins.MSBuild/GenerateRunConfig.cs new file mode 100644 index 0000000..c2e6aaa --- /dev/null +++ b/CringePlugins.MSBuild/GenerateRunConfig.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Win32; + +namespace CringePlugins.MSBuild; + +public class GenerateRunConfig : Task +{ + [Required] public required string RunConfigPath { get; set; } + + [Required] public required string ProjectName { get; set; } + + private const string RunConfigTemplate = """ + { + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "$projectName$ Dev Client": { + "commandName": "Executable", + "executablePath": "BootstrapExecutablePathPlaceholder", + "commandLineArgs": "\"GameExecutablePathPlaceholder\"", + "environmentVariables": { + "DOTNET_BOOTSTRAP_ENTRYPOINT": "CringeLauncher.UserDev.UserDevLauncher, CringeLauncher", + "DOTNET_USERDEV_RUNDIR": "data", + "DOTNET_USERDEV_PLUGINDIR": "." + } + } + } + } + """; + + public override bool Execute() + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + Log.LogError("Only windows is supported"); + return false; + } + + // branch off so jit wont notice in case Win32 package is missing from sdk distribution on non-windows platforms + // too lazy to test if it actually is missing + return ExecuteInternal(); + } + + private bool ExecuteInternal() + { + string? GetInstallLocation(RegistryKey baseKey, string subKey) + { + using var key = baseKey.OpenSubKey(subKey); + return key?.GetValue("InstallLocation") as string; + } + + var gamePath = GetInstallLocation(Registry.LocalMachine, + @"Software\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 244850"); + var bootstrapPath = GetInstallLocation(Registry.CurrentUser, + @"Software\Microsoft\Windows\CurrentVersion\Uninstall\CringeLauncher"); + + if (string.IsNullOrEmpty(gamePath)) + { + Log.LogError("Failed to find Space Engineers install location"); + return false; + } + + if (string.IsNullOrEmpty(bootstrapPath)) + { + Log.LogError("Failed to find CringeLauncher install location"); + return false; + } + + gamePath = Path.Combine(gamePath, @"Bin64\SpaceEngineers.exe").Replace(@"\", @"\\"); + bootstrapPath = Path.Combine(bootstrapPath, @"current\CringeBootstrap.exe").Replace(@"\", @"\\"); + + var runConfigText = RunConfigTemplate + .Replace("BootstrapExecutablePathPlaceholder", bootstrapPath) + .Replace("GameExecutablePathPlaceholder", gamePath) + .Replace("$projectName$", ProjectName); + + var runConfigDir = Path.GetDirectoryName(RunConfigPath); + if (runConfigDir is not null && !Directory.Exists(runConfigDir)) + Directory.CreateDirectory(runConfigDir); + + File.WriteAllText(RunConfigPath, runConfigText); + return true; + } +} \ No newline at end of file diff --git a/CringePlugins.MSBuild/build/CringePlugins.MSBuild.props b/CringePlugins.MSBuild/build/CringePlugins.MSBuild.props new file mode 100644 index 0000000..84ac82b --- /dev/null +++ b/CringePlugins.MSBuild/build/CringePlugins.MSBuild.props @@ -0,0 +1,14 @@ + + + + $(MSBuildThisFileDirectory)..\tasks\netstandard2.0 + + $(CustomTasksFolder)\$(MSBuildThisFileName).dll + + + + + + $(ProjectDir)Properties\launchSettings.json + + \ No newline at end of file diff --git a/CringePlugins.MSBuild/build/CringePlugins.MSBuild.targets b/CringePlugins.MSBuild/build/CringePlugins.MSBuild.targets new file mode 100644 index 0000000..e5499ea --- /dev/null +++ b/CringePlugins.MSBuild/build/CringePlugins.MSBuild.targets @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/CringePlugins.Templates/CringePlugins.Templates.csproj b/CringePlugins.Templates/CringePlugins.Templates.csproj new file mode 100644 index 0000000..105f732 --- /dev/null +++ b/CringePlugins.Templates/CringePlugins.Templates.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + False + False + False + $(ArtifactsTmpDir) + False + true + true + true + 2008;NU5105 + true + Template + True + 1.0.0 + + + + + + content + + + + diff --git a/CringePlugins.Templates/content/templates/Plugin/$projectName$.csproj b/CringePlugins.Templates/content/templates/Plugin/$projectName$.csproj new file mode 100644 index 0000000..2eed091 --- /dev/null +++ b/CringePlugins.Templates/content/templates/Plugin/$projectName$.csproj @@ -0,0 +1,32 @@ + + + + net9.0-windows + enable + enable + win-x64 + true + true + true + https://ng.zznty.ru/v3/index.json + true + CringePlugin + CringeLauncher + Plugin.$projectName$ + Plugin.$projectName$ + TitlePlaceholder + DescriptionPlaceholder + preview + icon.png + + + + + + + + + + + + \ No newline at end of file diff --git a/CringePlugins.Templates/content/templates/Plugin/.template.config/template.json b/CringePlugins.Templates/content/templates/Plugin/.template.config/template.json new file mode 100644 index 0000000..6616028 --- /dev/null +++ b/CringePlugins.Templates/content/templates/Plugin/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "zznty", + "classifications": [ "CringeLauncher", "SpaceEngineers", "Plugin" ], + "identity": "CringePlugins.PluginTemplate.Plugin", + "name": "SpaceEngineers Plugin Template", + "shortName": "cringeplugin", + "tags": { + "language": "C#", + "type": "project" + }, + "defaultName": "Plugin", + "sourceName": "$projectName$", + "postActions": [], + "symbols": { + "title": { + "type": "parameter", + "defaultValue": "Test Plugin", + "description": "The title of the plugin", + "replaces": "TitlePlaceholder" + }, + "description": { + "type": "parameter", + "defaultValue": "This is a test plugin", + "description": "The description of the plugin", + "replaces": "DescriptionPlaceholder" + } + } +} \ No newline at end of file diff --git a/CringePlugins.Templates/content/templates/Plugin/Plugin.cs b/CringePlugins.Templates/content/templates/Plugin/Plugin.cs new file mode 100644 index 0000000..ac7bd9d --- /dev/null +++ b/CringePlugins.Templates/content/templates/Plugin/Plugin.cs @@ -0,0 +1,18 @@ +using VRage.Plugins; + +namespace $projectName$; + +public class Plugin : IPlugin +{ + public void Init(object gameInstance) + { + } + + public void Update() + { + } + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/CringePlugins.Templates/content/templates/Plugin/icon.png b/CringePlugins.Templates/content/templates/Plugin/icon.png new file mode 100644 index 0000000..0fde5f7 Binary files /dev/null and b/CringePlugins.Templates/content/templates/Plugin/icon.png differ diff --git a/CringePlugins/Loader/PluginsLifetime.cs b/CringePlugins/Loader/PluginsLifetime.cs index ccd1565..86f1d38 100644 --- a/CringePlugins/Loader/PluginsLifetime.cs +++ b/CringePlugins/Loader/PluginsLifetime.cs @@ -15,7 +15,7 @@ using VRage.FileSystem; namespace CringePlugins.Loader; -internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : IPluginsLifetime +internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client, DirectoryInfo dir) : IPluginsLifetime { public static ImmutableArray Contexts { get; private set; } = []; private static readonly Lock ContextsLock = new(); @@ -25,8 +25,9 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : public string Name => "Loading Plugins"; private ImmutableArray _plugins = []; - private readonly DirectoryInfo _dir = Directory.CreateDirectory(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CringeLauncher")); - private readonly NuGetRuntimeFramework _runtimeFramework = new(NuGetFramework.ParseFolder("net9.0-windows10.0.19041.0"), RuntimeInformation.RuntimeIdentifier); + + private readonly NuGetRuntimeFramework _runtimeFramework = + new(NuGetFramework.ParseFolder("net9.0-windows10.0.19041.0"), RuntimeInformation.RuntimeIdentifier); private ConfigReference? _configReference; private ConfigReference? _launcherConfig; @@ -41,7 +42,7 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : await Task.Delay(10000); #endif - DiscoverLocalPlugins(_dir.CreateSubdirectory("plugins")); + DiscoverLocalPlugins(dir.CreateSubdirectory("plugins")); progress.Report("Loading config"); @@ -56,7 +57,7 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : // TODO take into account the target framework runtime identifier var resolver = new PackageResolver(_runtimeFramework.Framework, packagesConfig.Packages, sourceMapping); - var cacheDir = _dir.CreateSubdirectory("cache"); + var cacheDir = dir.CreateSubdirectory("cache"); var invalidPackages = new List(); var packages = await resolver.ResolveAsync(cacheDir, launcherConfig.DisablePluginUpdates, invalidPackages); @@ -78,7 +79,8 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : progress.Report("Downloading packages"); var builtInPackages = await BuiltInPackages.GetPackagesAsync(_runtimeFramework); - var cachedPackages = await PackageResolver.DownloadPackagesAsync(cacheDir, packages, builtInPackages.Keys.ToHashSet(), progress); + var cachedPackages = + await PackageResolver.DownloadPackagesAsync(cacheDir, packages, builtInPackages.Keys.ToHashSet(), progress); progress.Report("Loading plugins"); @@ -87,7 +89,8 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : await LoadPlugins(cachedPackages, sourceMapping, packagesConfig, builtInPackages); - RenderHandler.Current.RegisterComponent(new PluginListComponent(_configReference, _launcherConfig, sourceMapping, MyFileSystem.ExePath, _plugins)); + RenderHandler.Current.RegisterComponent(new PluginListComponent(_configReference, _launcherConfig, + sourceMapping, MyFileSystem.ExePath, _plugins)); } public static async Task ReloadPlugin(PluginInstance instance) @@ -96,7 +99,7 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : { var (oldContext, newContext) = await instance.ReloadAsync(); - lock(ContextsLock) + lock (ContextsLock) { Contexts = Contexts.Remove(oldContext).Add(newContext); } @@ -122,6 +125,7 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : Log.Error(e, "Failed to instantiate plugin {Plugin}", instance.Metadata); } } + Contexts = contextBuilder.ToImmutable(); } @@ -136,7 +140,7 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : resolvedPackages.TryAdd(package.Package.Id, package); } - var manifestBuilder = new DependencyManifestBuilder(_dir.CreateSubdirectory("cache"), sourceMapping, + var manifestBuilder = new DependencyManifestBuilder(dir.CreateSubdirectory("cache"), sourceMapping, dependency => { resolvedPackages.TryGetValue(dependency.Id, out var package); @@ -187,7 +191,9 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : { var plugins = ImmutableArray.Empty.ToBuilder(); - foreach (var directory in dir.EnumerateDirectories()) + foreach (var directory in Environment.GetEnvironmentVariable("DOTNET_USERDEV_PLUGINDIR") is { } userDevPlugin + ? [new(userDevPlugin), ..dir.GetDirectories()] + : dir.EnumerateDirectories()) { var files = directory.GetFiles("*.deps.json"); @@ -201,7 +207,8 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : _plugins = plugins.ToImmutable(); } - private static void LoadComponent(ImmutableArray.Builder plugins, string path, PluginMetadata? metadata = null, bool local = false) + private static void LoadComponent(ImmutableArray.Builder plugins, string path, + PluginMetadata? metadata = null, bool local = false) { try {