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
{