diff --git a/CringeBootstrap/Program.cs b/CringeBootstrap/Program.cs
index fbd3394..a1b05ae 100644
--- a/CringeBootstrap/Program.cs
+++ b/CringeBootstrap/Program.cs
@@ -25,7 +25,6 @@ var context = new GameDirectoryAssemblyLoadContext(dir);
// a list of assemblies which are not in the game binaries but reference them
context.AddDependencyOverride("CringeLauncher");
-context.AddDependencyOverride("PluginLoader");
context.AddDependencyOverride("CringePlugins");
var launcher = context.LoadFromAssemblyName(new AssemblyName("CringeLauncher"));
diff --git a/CringeBootstrap/packages.lock.json b/CringeBootstrap/packages.lock.json
index 749d537..3d42ef5 100644
--- a/CringeBootstrap/packages.lock.json
+++ b/CringeBootstrap/packages.lock.json
@@ -135,10 +135,15 @@
"resolved": "5.3.4",
"contentHash": "gLy7+O1hEYJXIlcTr1/VWjGXrZTQFZzYNO18IWasD64pNwz0BreV+nHLxWKXWZzERRzoKnsk2XYtwLkTVk7J1A=="
},
+ "NuGet.Frameworks": {
+ "type": "Transitive",
+ "resolved": "6.11.1",
+ "contentHash": "plTZ3ariSWQVsFn2mk83SsdmSg1VpgIMTSZpP/eSE/NNQF02p+M9ItxAYeUZBMX+cQ2nFkSwxQRJ0/fkaV9Hbg=="
+ },
"NuGet.Versioning": {
"type": "Transitive",
- "resolved": "6.10.1",
- "contentHash": "tovHZ3OlMVmsTdhv2z5nwnnhoA1ryhfJMyVQ9/+iv6d3h78fp230XaGy3K/iVcLwB50DdfNfIsitW97KSOWDFg=="
+ "resolved": "6.11.1",
+ "contentHash": "YNn3BB71F+guJW42TbAhGcMh3gpyqFMZcPVD9pm5vcvGivTALtRely/VCPWQQ6JQ5PfwIrjPaJMO7VnqyeK3rg=="
},
"protobuf-net": {
"type": "Transitive",
@@ -160,8 +165,8 @@
},
"SpaceEngineersDedicated.ReferenceAssemblies": {
"type": "Transitive",
- "resolved": "1.204.18",
- "contentHash": "GT7/9CBMx4jjor41zLOOl87YYM/JdJD8xp9ccXyuhP2oUaz25H3ZmCQuGeAuZNENKru1a/7hZrId4PwlMDGoew==",
+ "resolved": "1.205.23",
+ "contentHash": "J7mF5hY39PzzCZps6vhIRzKiq8vD6Af9TgumTJR068vjEi+BzyeEFhqX+cl2Dd1ngOmsBtGWc5m+vxgTfs5YuA==",
"dependencies": {
"SharpDX": "4.2.0-keen-cringe",
"protobuf-net": "1.0.0"
@@ -321,7 +326,11 @@
"CringeBootstrap.Abstractions": "[1.0.0, )",
"CringePlugins": "[1.0.0, )",
"ImGui.NET.DirectX": "[1.91.0.1, )",
- "PluginLoader": "[1.0.0, )",
+ "Lib.Harmony.Thin": "[2.3.3, )",
+ "Microsoft.CodeAnalysis.CSharp": "[4.11.0, )",
+ "NLog": "[5.3.4, )",
+ "SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )",
+ "Steamworks.NET": "[20.1.0, )",
"System.Diagnostics.PerformanceCounter": "[8.0.0, )",
"System.Management": "[8.0.0, )",
"System.Private.ServiceModel": "[4.10.3, )",
@@ -333,20 +342,26 @@
"cringeplugins": {
"type": "Project",
"dependencies": {
- "CringeBootstrap.Abstractions": "[1.0.0, )",
"NLog": "[5.3.4, )",
+ "NuGet": "[1.0.0, )",
+ "SharedCringe": "[1.0.0, )",
"SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )",
"dnlib": "[4.4.0, )"
}
},
- "pluginloader": {
+ "nuget": {
"type": "Project",
"dependencies": {
- "Lib.Harmony.Thin": "[2.3.3, )",
- "Microsoft.CodeAnalysis.CSharp": "[4.11.0, )",
+ "NuGet.Frameworks": "[6.11.1, )",
+ "NuGet.Versioning": "[6.11.1, )"
+ }
+ },
+ "sharedcringe": {
+ "type": "Project",
+ "dependencies": {
+ "CringeBootstrap.Abstractions": "[1.0.0, )",
"NLog": "[5.3.4, )",
- "SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )",
- "Steamworks.NET": "[20.1.0, )"
+ "SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )"
}
}
},
diff --git a/CringeLauncher.sln b/CringeLauncher.sln
index c9d9a67..60cb0a0 100644
--- a/CringeLauncher.sln
+++ b/CringeLauncher.sln
@@ -12,6 +12,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringePlugins", "CringePlug
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "TestPlugin\TestPlugin.csproj", "{C1670878-2301-4AE5-ABD3-5C4D0882CB02}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet", "NuGet\NuGet.csproj", "{CC5362CA-881A-4867-9642-033269C1F7D7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedCringe", "SharedCringe\SharedCringe.csproj", "{BDA680D8-D4E3-48A9-8ED4-C4991098B71D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -42,5 +46,13 @@ Global
{C1670878-2301-4AE5-ABD3-5C4D0882CB02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1670878-2301-4AE5-ABD3-5C4D0882CB02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1670878-2301-4AE5-ABD3-5C4D0882CB02}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC5362CA-881A-4867-9642-033269C1F7D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC5362CA-881A-4867-9642-033269C1F7D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC5362CA-881A-4867-9642-033269C1F7D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC5362CA-881A-4867-9642-033269C1F7D7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDA680D8-D4E3-48A9-8ED4-C4991098B71D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDA680D8-D4E3-48A9-8ED4-C4991098B71D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDA680D8-D4E3-48A9-8ED4-C4991098B71D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDA680D8-D4E3-48A9-8ED4-C4991098B71D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/CringeLauncher/CringeLauncher.csproj b/CringeLauncher/CringeLauncher.csproj
index 5a49894..e4aeac6 100644
--- a/CringeLauncher/CringeLauncher.csproj
+++ b/CringeLauncher/CringeLauncher.csproj
@@ -13,6 +13,10 @@
+
+
+
+
@@ -32,12 +36,16 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
-
diff --git a/CringeLauncher/ImGuiHandler.cs b/CringeLauncher/ImGuiHandler.cs
index 6be3f64..454fbfb 100644
--- a/CringeLauncher/ImGuiHandler.cs
+++ b/CringeLauncher/ImGuiHandler.cs
@@ -4,6 +4,7 @@ using System.Runtime.Versioning;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
+using CringePlugins.Abstractions;
using CringePlugins.Render;
using ImGuiNET;
using SharpDX.Direct3D11;
@@ -19,7 +20,7 @@ internal class ImGuiHandler : IDisposable
public static ImGuiHandler? Instance;
public static RenderTargetView? Rtv;
- private readonly RenderHandler _renderHandler;
+ private readonly IRootRenderComponent _renderHandler;
public ImGuiHandler(nint windowHandle, Device1 device, DeviceContext deviceContext)
{
diff --git a/CringeLauncher/Launcher.cs b/CringeLauncher/Launcher.cs
index ec71d08..8e77203 100644
--- a/CringeLauncher/Launcher.cs
+++ b/CringeLauncher/Launcher.cs
@@ -1,13 +1,11 @@
-using System.Diagnostics;
-using System.Reflection;
-using System.Runtime.Loader;
+using System.Reflection;
using CringeBootstrap.Abstractions;
using CringeLauncher.Utils;
using CringePlugins.Loader;
+using CringePlugins.Splash;
using HarmonyLib;
using NLog;
using Sandbox;
-using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Sandbox.Engine.Platform.VideoMode;
using Sandbox.Engine.Utils;
@@ -15,21 +13,19 @@ using Sandbox.Game;
using SpaceEngineers.Game;
using SpaceEngineers.Game.Achievements;
using SpaceEngineers.Game.GUI;
-using Steamworks;
using Velopack;
using VRage;
using VRage.Audio;
-using VRage.EOS;
using VRage.FileSystem;
using VRage.Game;
using VRage.Game.Localization;
using VRage.GameServices;
using VRage.Mod.Io;
using VRage.Platform.Windows;
-using VRage.Plugins;
using VRage.Steam;
using VRage.UserInterface;
using VRageRender;
+using VRageRender.ExternalApp;
namespace CringeLauncher;
@@ -40,6 +36,8 @@ public class Launcher : ICorePlugin
private readonly Harmony _harmony = new("CringeBootstrap");
private PluginsLifetime? _lifetime;
+ private MyGameRenderComponent? _renderComponent;
+
public void Initialize(string[] args)
{
LogManager.Setup()
@@ -57,33 +55,25 @@ public class Launcher : ICorePlugin
//environment variable for viktor's plugins
Environment.SetEnvironmentVariable("SE_PLUGIN_DISABLE_METHOD_VERIFICATION", "True");
-
- // early init for plugin loader
- ProtoBuf.Meta.RuntimeTypeModel.Create(true);
+
_harmony.PatchAll(typeof(Launcher).Assembly);
- SteamAPI.Init();
- MyPlugins.LoadPlugins([typeof(PluginLoader.Main).Assembly]);
#if !DEBUG
CheckUpdates().GetAwaiter().GetResult();
#endif
+
+ var splash = new Splash();
- PluginLoader.Main.Instance.Splash?.SetText("Initializing plugins...");
- PluginLoader.Main.Instance.Splash?.SetBarValue(0);
+ splash.DefineStage(_lifetime = new PluginsLifetime());
- _lifetime = new PluginsLifetime();
-
- PluginLoader.Main.Instance.Splash?.SetText("Initializing game...");
InitTexts();
SpaceEngineersGame.SetupBasicGameInfo();
MyFinalBuildConstants.APP_VERSION = MyPerGameSettings.BasicGameInfo.GameVersion.GetValueOrDefault();
MyShaderCompiler.Init(MyShaderCompiler.TargetPlatform.PC, false);
- PluginLoader.Main.Instance.Splash?.SetBarValue(.25f);
MyVRageWindows.Init(MyPerGameSettings.BasicGameInfo.ApplicationName, MySandboxGame.Log,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
MyPerGameSettings.BasicGameInfo.ApplicationName),
false, false);
- PluginLoader.Main.Instance.Splash?.SetBarValue(.35f);
MyPlatformGameSettings.SAVE_TO_CLOUD_OPTION_AVAILABLE = true;
MyXAudio2.DEVICE_DETAILS_SUPPORTED = false;
@@ -101,39 +91,54 @@ public class Launcher : ICorePlugin
MyFakes.VOICE_CHAT_MIC_SENSITIVITY = MySandboxGame.Config.MicSensitivity;
MyPlatformGameSettings.VOICE_CHAT_AUTOMATIC_ACTIVATION = MySandboxGame.Config.AutomaticVoiceChatActivation;
});
- PluginLoader.Main.Instance.Splash?.SetBarValue(.55f);
MyVRage.Platform.Init();
- PluginLoader.Main.Instance.Splash?.SetBarValue(.65f);
- InitUgc();
- PluginLoader.Main.Instance.Splash?.SetBarValue(.75f);
SpaceEngineersGame.SetupPerGameSettings();
ConfigureSettings();
- // MySandboxGame.InitMultithreading();
InitThreadPool();
- PluginLoader.Main.Instance.Splash?.SetBarValue(.85f);
MyVRage.Platform.System.OnThreadpoolInitialized();
InitRender();
- PluginLoader.Main.Instance.Splash?.SetBarValue(.95f);
+
+ _renderComponent = new();
+ _renderComponent.Start(new(), InitEarlyWindow, MyVideoSettingsManager.Initialize(), MyPerGameSettings.MaxFrameRate);
+ _renderComponent.RenderThread.BeforeDraw += MyFpsManager.Update;
+ _renderComponent.RenderThread.SizeChanged += RenderThreadOnSizeChanged;
+
+ splash.ExecuteLoadingStages();
+
+ InitUgc();
MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString());
- _game = new(args);
- PluginLoader.Main.Instance.Splash?.SetText("Launching...");
+ _game = new(args)
+ {
+ GameRenderComponent = _renderComponent,
+ DrawThread = _renderComponent.RenderThread.SystemThread,
+ form = MyVRage.Platform.Windows.Window
+ };
+
+ _renderComponent.RenderThread.SizeChanged += _game.RenderThread_SizeChanged;
+ _renderComponent.RenderThread.UpdateSize();
+ }
+
+ private void RenderThreadOnSizeChanged(int width, int height, MyViewport viewport)
+ {
+ if (_renderComponent is not null)
+ _renderComponent.RenderThread.SizeChanged -= RenderThreadOnSizeChanged;
+
+ MyVRage.Platform.Windows.Window.ShowAndFocus();
}
public void Run() => _game?.Run();
+ private static IVRageWindow InitEarlyWindow()
+ {
+ MyVRage.Platform.Windows.CreateWindow("Cringe Launcher", MyPerGameSettings.GameIcon, null);
+
+ MyVRage.Platform.Windows.Window.OnExit += MySandboxGame.ExitThreadSafe;
+
+ return MyVRage.Platform.Windows.Window;
+ }
+
private async Task CheckUpdates()
{
- void Report(string s, float p)
- {
- PluginLoader.Main.Instance.Splash?.Invoke(() =>
- {
- PluginLoader.Main.Instance.Splash?.SetText(s);
- PluginLoader.Main.Instance.Splash?.SetBarValue(p);
- });
- }
-
- PluginLoader.Main.Instance.Splash?.SetText("Checking for updates...");
-
var mgr = new UpdateManager("https://the.place/you-host/updates");
// check for new version
@@ -142,9 +147,7 @@ public class Launcher : ICorePlugin
return; // no update available
// download new version
- await mgr.DownloadUpdatesAsync(newVersion, i => Report("Downloading update...", i / 100f));
-
- Report("Restarting for update", 1f);
+ await mgr.DownloadUpdatesAsync(newVersion);
// install new version and restart app
mgr.ApplyUpdatesAndRestart(newVersion);
@@ -164,8 +167,7 @@ public class Launcher : ICorePlugin
private static void InitTexts()
{
- //MyLanguage.Instance.ObtainCurrentOSCulture();
- var textsPath = Path.Combine(MyFileSystem.RootPath, "Content\\Data\\Localization\\CoreTexts");
+ var textsPath = Path.Combine(MyFileSystem.RootPath, @"Content\Data\Localization\CoreTexts");
var hashSet = new HashSet();
MyTexts.LoadSupportedLanguages(textsPath, hashSet);
@@ -176,7 +178,7 @@ public class Launcher : ICorePlugin
MyTexts.LoadTexts(textsPath, description.CultureName, description.SubcultureName);
}
- private static void InitUgc()
+ public static void InitUgc()
{
var steamGameService = MySteamGameService.Create(false, AppId);
MyServiceManager.Instance.AddService(steamGameService);
diff --git a/CringeLauncher/Loader/ModAssemblyLoadContext.cs b/CringeLauncher/Loader/ModAssemblyLoadContext.cs
new file mode 100644
index 0000000..47429e9
--- /dev/null
+++ b/CringeLauncher/Loader/ModAssemblyLoadContext.cs
@@ -0,0 +1,7 @@
+using CringeBootstrap.Abstractions;
+using SharedCringe.Loader;
+
+namespace CringeLauncher.Loader;
+
+public class ModAssemblyLoadContext(ICoreLoadContext parentContext)
+ : DerivedAssemblyLoadContext(parentContext, "World Mods Context");
\ No newline at end of file
diff --git a/CringeLauncher/Patches/IntrospectionPatches.cs b/CringeLauncher/Patches/IntrospectionPatches.cs
index 6c8530e..fdf553d 100644
--- a/CringeLauncher/Patches/IntrospectionPatches.cs
+++ b/CringeLauncher/Patches/IntrospectionPatches.cs
@@ -1,15 +1,18 @@
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Loader;
+using System.Xml.Serialization;
using CringeBootstrap.Abstractions;
using CringePlugins.Utils;
using HarmonyLib;
+using Sandbox.Game.World;
using VRage;
using VRage.FileSystem;
using VRage.Game;
using VRage.Game.Common;
using VRage.Game.Components;
using VRage.Game.Definitions;
+using VRage.Game.Entity.UseObject;
using VRage.ObjectBuilders;
using VRage.ObjectBuilders.Private;
using VRage.Plugins;
@@ -24,15 +27,31 @@ public static class IntrospectionPatches
{
if (AssemblyLoadContext.GetLoadContext(__instance) is ICoreLoadContext || __instance.FullName?.StartsWith("System.") == true)
return true;
-
-#if DEBUG
- Debugger.Break();
+
+ Debug.WriteLine($"Blocking GetTypes for {__instance.FullName}");
__result = [];
return false;
-#else
- throw new NotSupportedException($"Getting types from {__instance} is not supported");
-#endif
+ }
+
+ [HarmonyPrefix, HarmonyPatch(typeof(AccessTools), nameof(AccessTools.GetTypesFromAssembly))]
+ private static bool GetTypesHarmonyPrefix(Assembly assembly, ref Type[] __result)
+ {
+ if (AssemblyLoadContext.GetLoadContext(assembly) is ICoreLoadContext)
+ return true;
+
+ __result = IntrospectionContext.Global.CollectAttributedTypes(assembly.GetMainModule())
+ .ToArray();
+ return false;
+ }
+
+ [HarmonyPrefix, HarmonyPatch(typeof(MySession), "PrepareBaseSession", typeof(MyObjectBuilder_Checkpoint), typeof(MyObjectBuilder_Sector))]
+ private static void PrepareSessionPrefix()
+ {
+ // i hate keen for that in MyUseObjectFactory..cctor
+ // MyUseObjectFactory.RegisterAssemblyTypes(Assembly.LoadFrom(Path.Combine(MyFileSystem.ExePath, "Sandbox.Game.dll")));
+
+ MyUseObjectFactory.RegisterAssemblyTypes(MyPlugins.SandboxGameAssembly);
}
[HarmonyPrefix, HarmonyPatch(typeof(MyPlugins), nameof(MyPlugins.LoadPlugins))]
@@ -52,83 +71,24 @@ public static class IntrospectionPatches
return false;
}
- [HarmonyPrefix, HarmonyPatch(typeof(MyObjectBuilderSerializerKeen), nameof(MyObjectBuilderSerializerKeen.RegisterFromAssembly))]
- private static bool RegisterXmlSerializersPrefix(
- MyObjectFactory ___m_objectFactory, Assembly assembly)
+ [HarmonyPrefix, HarmonyPatch(typeof(MyXmlSerializerManager), "TryLoadSerializerFrom")]
+ private static bool LoadSerializerPrefix(string assemblyName, string typeName, ref XmlSerializer? __result)
{
- Register(___m_objectFactory, assembly);
- return false;
- }
-
- [HarmonyPrefix, HarmonyPatch(typeof(MyComponentFactory), nameof(MyComponentFactory.RegisterFromAssembly))]
- private static bool RegisterComponentsPrefix(
- MyObjectFactory ___m_objectFactory, Assembly assembly)
- {
- Register(___m_objectFactory, assembly);
- return false;
- }
+ if (AssemblyLoadContext.GetLoadContext(typeof(IntrospectionPatches).Assembly) is not ICoreLoadContext context)
+ return false;
- [HarmonyTranspiler,
- HarmonyPatch(typeof(MyDefinitionManagerBase), nameof(MyDefinitionManagerBase.RegisterTypesFromAssembly))]
- private static IEnumerable RegisterDefinitionTypesTranspiler(
- IEnumerable instructions)
- {
- var getTypesMethod = AccessTools.DeclaredMethod(typeof(Assembly), nameof(Assembly.GetTypes));
- var getDefinitionTypesMethod = AccessTools.DeclaredMethod(typeof(IntrospectionPatches), nameof(GetDefinitionTypes));
- return instructions.MethodReplacer(getTypesMethod, getDefinitionTypesMethod);
- }
+ var assembly = context.ResolveFromAssemblyName(new(assemblyName));
- private static Type[] GetDefinitionTypes(Assembly assembly)
- {
- return IntrospectionContext.Global.CollectAttributedTypes(assembly.GetMainModule())
- .ToArray();
- }
-
- [HarmonyTranspiler,
- HarmonyPatch(typeof(MyObjectBuilderType), nameof(MyObjectBuilderType.RegisterFromAssembly))]
- private static IEnumerable RegisterObjectBuilderTypesTranspiler(
- IEnumerable instructions)
- {
- var getTypesMethod = AccessTools.DeclaredMethod(typeof(Assembly), nameof(Assembly.GetTypes));
- var getDefinitionTypesMethod = AccessTools.DeclaredMethod(typeof(IntrospectionPatches), nameof(GetObjectBuilderTypes));
- return instructions.MethodReplacer(getTypesMethod, getDefinitionTypesMethod);
- }
-
- private static Type[] GetObjectBuilderTypes(Assembly assembly)
- {
- return IntrospectionContext.Global.CollectDerivedTypes(assembly.GetMainModule())
- .ToArray();
- }
-
- [HarmonyTranspiler,
- HarmonyPatch(typeof(MyComponentTypeFactory), nameof(MyComponentTypeFactory.RegisterFromAssembly), typeof(Assembly))]
- private static IEnumerable RegisterComponentTypesTranspiler(IEnumerable instructions)
- {
- var getTypesMethod = AccessTools.DeclaredMethod(typeof(Assembly), nameof(Assembly.GetTypes));
- var getDefinitionTypesMethod = AccessTools.DeclaredMethod(typeof(IntrospectionPatches), nameof(GetComponentTypes));
- return instructions.MethodReplacer(getTypesMethod, getDefinitionTypesMethod);
- }
-
- private static Type[] GetComponentTypes(Assembly assembly)
- {
- return IntrospectionContext.Global.CollectDerivedTypes(assembly.GetMainModule())
- .ToArray();
+ if (assembly?.GetType($"Microsoft.Xml.Serialization.GeneratedAssembly.{typeName}Serializer") is not { } type)
+ return false;
+
+ __result = Activator.CreateInstance(type) as XmlSerializer;
+
+ return false;
}
[HarmonyPrefix, HarmonyPatch(typeof(MyXmlSerializerManager), nameof(MyXmlSerializerManager.RegisterFromAssembly))]
private static bool XmlManagerRegisterPrefix(Assembly assembly) => AssemblyLoadContext.GetLoadContext(assembly) is ICoreLoadContext;
-
- private static void Register(MyObjectFactory factory, Assembly assembly)
- where TAttribute : MyFactoryTagAttribute where TCreatedObjectBase : class
- {
- foreach (var type in IntrospectionContext.Global.CollectAttributedTypes(assembly.GetMainModule()))
- {
- foreach (var attribute in type.GetCustomAttributes())
- {
- factory.RegisterDescriptor(attribute, type);
- }
- }
- }
[HarmonyPatch]
private static class GameAssembliesPatch
diff --git a/CringeLauncher/Patches/ModAssemblyLoadContextPatches.cs b/CringeLauncher/Patches/ModAssemblyLoadContextPatches.cs
new file mode 100644
index 0000000..2222a32
--- /dev/null
+++ b/CringeLauncher/Patches/ModAssemblyLoadContextPatches.cs
@@ -0,0 +1,70 @@
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.Loader;
+using CringeBootstrap.Abstractions;
+using CringeLauncher.Loader;
+using HarmonyLib;
+using Sandbox.Game.World;
+using VRage.Scripting;
+
+namespace CringeLauncher.Patches;
+
+[HarmonyPatch]
+public static class ModAssemblyLoadContextPatches
+{
+ private static ModAssemblyLoadContext? _currentSessionContext;
+
+ [HarmonyPatch(typeof(MyScriptCompiler), nameof(MyScriptCompiler.Compile), MethodType.Async)]
+ [HarmonyTranspiler]
+ private static IEnumerable CompilerTranspiler(IEnumerable instructions, MethodBase original)
+ {
+ var matcher = new CodeMatcher(instructions);
+
+ var load1Method = AccessTools.DeclaredMethod(typeof(Assembly), nameof(Assembly.Load), [typeof(byte[]), typeof(byte[])]);
+ var load2Method = AccessTools.DeclaredMethod(typeof(Assembly), nameof(Assembly.Load), [typeof(byte[])]);
+
+ matcher.SearchForward(i => i.Calls(load1Method))
+ .InsertAndAdvance(new(OpCodes.Ldarg_0), CodeInstruction.LoadField(original.DeclaringType, "target"))
+ .SetInstruction(CodeInstruction.CallClosure((byte[] assembly, byte[] symbols, MyApiTarget target) =>
+ {
+ if (target is not MyApiTarget.Mod) return Assembly.Load(assembly, symbols);
+ ArgumentNullException.ThrowIfNull(_currentSessionContext, "No session context");
+ return _currentSessionContext.LoadFromStream(new MemoryStream(assembly), new MemoryStream(symbols));
+ }));
+
+ matcher.SearchForward(i => i.Calls(load2Method))
+ .InsertAndAdvance(new(OpCodes.Ldarg_0), CodeInstruction.LoadField(original.DeclaringType, "target"))
+ .SetInstruction(CodeInstruction.CallClosure((byte[] assembly, MyApiTarget target) =>
+ {
+ if (target is not MyApiTarget.Mod) return Assembly.Load(assembly);
+ ArgumentNullException.ThrowIfNull(_currentSessionContext, "No session context");
+ return _currentSessionContext.LoadFromStream(new MemoryStream(assembly));
+ }));
+
+ return matcher.Instructions();
+ }
+
+ [HarmonyPatch(typeof(MySession), nameof(MySession.Load))]
+ [HarmonyPatch(typeof(MySession), "LoadMultiplayer")]
+ [HarmonyPrefix]
+ private static void LoadPrefix()
+ {
+ if (_currentSessionContext is not null)
+ throw new InvalidOperationException("Previous session context was not disposed");
+
+ if (AssemblyLoadContext.GetLoadContext(typeof(MySession).Assembly) is not ICoreLoadContext coreContext)
+ throw new NotSupportedException("Mod loading is not supported in this context");
+
+ _currentSessionContext = new ModAssemblyLoadContext(coreContext);
+ }
+
+ [HarmonyPatch(typeof(MySession), nameof(MySession.Unload))]
+ [HarmonyPostfix]
+ private static void UnloadPostfix()
+ {
+ if (_currentSessionContext is null) return;
+
+ _currentSessionContext.Unload();
+ _currentSessionContext = null;
+ }
+}
\ No newline at end of file
diff --git a/CringeLauncher/Patches/RenderInitPatch.cs b/CringeLauncher/Patches/RenderInitPatch.cs
new file mode 100644
index 0000000..490676e
--- /dev/null
+++ b/CringeLauncher/Patches/RenderInitPatch.cs
@@ -0,0 +1,10 @@
+using HarmonyLib;
+using SpaceEngineers.Game;
+
+namespace CringeLauncher.Patches;
+
+[HarmonyPatch(typeof(SpaceEngineersGame), "InitializeRender")]
+public static class RenderInitPatch
+{
+ private static bool Prefix() => false;
+}
\ No newline at end of file
diff --git a/CringeLauncher/packages.lock.json b/CringeLauncher/packages.lock.json
index ac8a9e0..107d8c4 100644
--- a/CringeLauncher/packages.lock.json
+++ b/CringeLauncher/packages.lock.json
@@ -25,6 +25,28 @@
"resolved": "2.2.1",
"contentHash": "QGI4nMGQbKsuFUUboixVHu4mv3lHB5RejIa7toIlzTmwLkuCYYEpUBJjmy3OpXYyj5dVSZAXVbr4oeMSloE67Q=="
},
+ "Lib.Harmony.Thin": {
+ "type": "Direct",
+ "requested": "[2.3.3, )",
+ "resolved": "2.3.3",
+ "contentHash": "jsaFv7XnWJnyfyvFbkgIkZtV6tWMteNUcDK3idq+3LwPqpTFNxsOv2eKmj4qqP8QR8UynG1Y9AUaC/+dVruMHg==",
+ "dependencies": {
+ "MonoMod.Core": "1.1.0",
+ "System.Text.Json": "8.0.1"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp": {
+ "type": "Direct",
+ "requested": "[4.11.0, )",
+ "resolved": "4.11.0",
+ "contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.3.4",
+ "Microsoft.CodeAnalysis.Common": "[4.11.0]",
+ "System.Collections.Immutable": "8.0.0",
+ "System.Reflection.Metadata": "8.0.0"
+ }
+ },
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.106, )",
@@ -36,6 +58,28 @@
"Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental"
}
},
+ "NLog": {
+ "type": "Direct",
+ "requested": "[5.3.4, )",
+ "resolved": "5.3.4",
+ "contentHash": "gLy7+O1hEYJXIlcTr1/VWjGXrZTQFZzYNO18IWasD64pNwz0BreV+nHLxWKXWZzERRzoKnsk2XYtwLkTVk7J1A=="
+ },
+ "SpaceEngineersDedicated.ReferenceAssemblies": {
+ "type": "Direct",
+ "requested": "[1.*, )",
+ "resolved": "1.205.23",
+ "contentHash": "J7mF5hY39PzzCZps6vhIRzKiq8vD6Af9TgumTJR068vjEi+BzyeEFhqX+cl2Dd1ngOmsBtGWc5m+vxgTfs5YuA==",
+ "dependencies": {
+ "SharpDX": "4.2.0-keen-cringe",
+ "protobuf-net": "1.0.0"
+ }
+ },
+ "Steamworks.NET": {
+ "type": "Direct",
+ "requested": "[20.1.0, )",
+ "resolved": "20.1.0",
+ "contentHash": "+GntwnyJ5tCNvUIaQxv2+ehDvZJzGUqlSB5xRBk1hTj1qqBJ6s4vK/OfGD/jae7aTmXiGSm8wpJORosNtQevJQ=="
+ },
"System.Diagnostics.PerformanceCounter": {
"type": "Direct",
"requested": "[8.0.0, )",
@@ -88,15 +132,6 @@
"NuGet.Versioning": "6.10.1"
}
},
- "Lib.Harmony.Thin": {
- "type": "Transitive",
- "resolved": "2.3.3",
- "contentHash": "jsaFv7XnWJnyfyvFbkgIkZtV6tWMteNUcDK3idq+3LwPqpTFNxsOv2eKmj4qqP8QR8UynG1Y9AUaC/+dVruMHg==",
- "dependencies": {
- "MonoMod.Core": "1.1.0",
- "System.Text.Json": "8.0.1"
- }
- },
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -117,17 +152,6 @@
"System.Reflection.Metadata": "8.0.0"
}
},
- "Microsoft.CodeAnalysis.CSharp": {
- "type": "Transitive",
- "resolved": "4.11.0",
- "contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==",
- "dependencies": {
- "Microsoft.CodeAnalysis.Analyzers": "3.3.4",
- "Microsoft.CodeAnalysis.Common": "[4.11.0]",
- "System.Collections.Immutable": "8.0.0",
- "System.Reflection.Metadata": "8.0.0"
- }
- },
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -203,15 +227,15 @@
"MonoMod.ILHelpers": "1.0.1"
}
},
- "NLog": {
+ "NuGet.Frameworks": {
"type": "Transitive",
- "resolved": "5.3.4",
- "contentHash": "gLy7+O1hEYJXIlcTr1/VWjGXrZTQFZzYNO18IWasD64pNwz0BreV+nHLxWKXWZzERRzoKnsk2XYtwLkTVk7J1A=="
+ "resolved": "6.11.1",
+ "contentHash": "plTZ3ariSWQVsFn2mk83SsdmSg1VpgIMTSZpP/eSE/NNQF02p+M9ItxAYeUZBMX+cQ2nFkSwxQRJ0/fkaV9Hbg=="
},
"NuGet.Versioning": {
"type": "Transitive",
- "resolved": "6.10.1",
- "contentHash": "tovHZ3OlMVmsTdhv2z5nwnnhoA1ryhfJMyVQ9/+iv6d3h78fp230XaGy3K/iVcLwB50DdfNfIsitW97KSOWDFg=="
+ "resolved": "6.11.1",
+ "contentHash": "YNn3BB71F+guJW42TbAhGcMh3gpyqFMZcPVD9pm5vcvGivTALtRely/VCPWQQ6JQ5PfwIrjPaJMO7VnqyeK3rg=="
},
"protobuf-net": {
"type": "Transitive",
@@ -231,20 +255,6 @@
"System.Runtime.CompilerServices.Unsafe": "4.5.1"
}
},
- "SpaceEngineersDedicated.ReferenceAssemblies": {
- "type": "Transitive",
- "resolved": "1.204.18",
- "contentHash": "GT7/9CBMx4jjor41zLOOl87YYM/JdJD8xp9ccXyuhP2oUaz25H3ZmCQuGeAuZNENKru1a/7hZrId4PwlMDGoew==",
- "dependencies": {
- "SharpDX": "4.2.0-keen-cringe",
- "protobuf-net": "1.0.0"
- }
- },
- "Steamworks.NET": {
- "type": "Transitive",
- "resolved": "20.1.0",
- "contentHash": "+GntwnyJ5tCNvUIaQxv2+ehDvZJzGUqlSB5xRBk1hTj1qqBJ6s4vK/OfGD/jae7aTmXiGSm8wpJORosNtQevJQ=="
- },
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
@@ -353,20 +363,26 @@
"cringeplugins": {
"type": "Project",
"dependencies": {
- "CringeBootstrap.Abstractions": "[1.0.0, )",
"NLog": "[5.3.4, )",
+ "NuGet": "[1.0.0, )",
+ "SharedCringe": "[1.0.0, )",
"SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )",
"dnlib": "[4.4.0, )"
}
},
- "pluginloader": {
+ "nuget": {
"type": "Project",
"dependencies": {
- "Lib.Harmony.Thin": "[2.3.3, )",
- "Microsoft.CodeAnalysis.CSharp": "[4.11.0, )",
+ "NuGet.Frameworks": "[6.11.1, )",
+ "NuGet.Versioning": "[6.11.1, )"
+ }
+ },
+ "sharedcringe": {
+ "type": "Project",
+ "dependencies": {
+ "CringeBootstrap.Abstractions": "[1.0.0, )",
"NLog": "[5.3.4, )",
- "SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )",
- "Steamworks.NET": "[20.1.0, )"
+ "SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )"
}
}
},
@@ -382,6 +398,12 @@
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
+ "Steamworks.NET": {
+ "type": "Direct",
+ "requested": "[20.1.0, )",
+ "resolved": "20.1.0",
+ "contentHash": "+GntwnyJ5tCNvUIaQxv2+ehDvZJzGUqlSB5xRBk1hTj1qqBJ6s4vK/OfGD/jae7aTmXiGSm8wpJORosNtQevJQ=="
+ },
"System.Diagnostics.PerformanceCounter": {
"type": "Direct",
"requested": "[8.0.0, )",
@@ -400,11 +422,6 @@
"System.CodeDom": "8.0.0"
}
},
- "Steamworks.NET": {
- "type": "Transitive",
- "resolved": "20.1.0",
- "contentHash": "+GntwnyJ5tCNvUIaQxv2+ehDvZJzGUqlSB5xRBk1hTj1qqBJ6s4vK/OfGD/jae7aTmXiGSm8wpJORosNtQevJQ=="
- },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.0",
diff --git a/CringePlugins/Abstractions/IRenderComponent.cs b/CringePlugins/Abstractions/IRenderComponent.cs
index 67f556b..d679ee8 100644
--- a/CringePlugins/Abstractions/IRenderComponent.cs
+++ b/CringePlugins/Abstractions/IRenderComponent.cs
@@ -2,5 +2,10 @@
public interface IRenderComponent
{
+ ///
+ /// Gets called while the dear ImGui frame is being composed
+ ///
void OnFrame();
-}
\ No newline at end of file
+}
+
+internal interface IRootRenderComponent : IRenderComponent, IDisposable;
\ No newline at end of file
diff --git a/CringePlugins/Config/PackagesConfig.cs b/CringePlugins/Config/PackagesConfig.cs
new file mode 100644
index 0000000..9d7c8db
--- /dev/null
+++ b/CringePlugins/Config/PackagesConfig.cs
@@ -0,0 +1,13 @@
+using System.Collections.Immutable;
+using CringePlugins.Resolver;
+using NuGet;
+using NuGet.Models;
+
+namespace CringePlugins.Config;
+
+public record PackagesConfig(ImmutableArray Sources, ImmutableArray Packages)
+{
+ public static PackagesConfig Default { get; } = new([
+ new(string.Empty, "https://api.nuget.org/v3/index.json")
+ ], []);
+}
\ No newline at end of file
diff --git a/CringePlugins/CringePlugins.csproj b/CringePlugins/CringePlugins.csproj
index e0e6f91..da11274 100644
--- a/CringePlugins/CringePlugins.csproj
+++ b/CringePlugins/CringePlugins.csproj
@@ -1,14 +1,16 @@
- net8.0
+ net8.0-windows
enable
enable
+ true
true
-
+
+
@@ -25,5 +27,9 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/CringePlugins/Loader/PluginAssemblyLoadContext.cs b/CringePlugins/Loader/PluginAssemblyLoadContext.cs
index c43264b..c587f84 100644
--- a/CringePlugins/Loader/PluginAssemblyLoadContext.cs
+++ b/CringePlugins/Loader/PluginAssemblyLoadContext.cs
@@ -1,19 +1,18 @@
using System.Reflection;
using System.Runtime.Loader;
using CringeBootstrap.Abstractions;
+using SharedCringe.Loader;
namespace CringePlugins.Loader;
-internal class PluginAssemblyLoadContext : AssemblyLoadContext
+internal class PluginAssemblyLoadContext : DerivedAssemblyLoadContext
{
- private readonly ICoreLoadContext _parentContext;
private readonly string _entrypointPath;
private readonly AssemblyDependencyResolver _dependencyResolver;
private Assembly? _assembly;
- internal PluginAssemblyLoadContext(ICoreLoadContext parentContext, string entrypointPath) : base($"Plugin Context {Path.GetFileNameWithoutExtension(entrypointPath)}", true)
+ internal PluginAssemblyLoadContext(ICoreLoadContext parentContext, string entrypointPath) : base(parentContext, $"Plugin Context {Path.GetFileNameWithoutExtension(entrypointPath)}")
{
- _parentContext = parentContext;
_entrypointPath = entrypointPath;
_dependencyResolver = new(entrypointPath);
}
@@ -25,7 +24,7 @@ internal class PluginAssemblyLoadContext : AssemblyLoadContext
if (_dependencyResolver.ResolveAssemblyToPath(assemblyName) is { } path)
return LoadFromAssemblyPath(path);
- return _parentContext.ResolveFromAssemblyName(assemblyName);
+ return base.Load(assemblyName);
}
protected override nint LoadUnmanagedDll(string unmanagedDllName)
@@ -33,6 +32,6 @@ internal class PluginAssemblyLoadContext : AssemblyLoadContext
if (_dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName) is { } path)
return LoadUnmanagedDllFromPath(path);
- return _parentContext.ResolveUnmanagedDll(unmanagedDllName);
+ return base.LoadUnmanagedDll(unmanagedDllName);
}
}
\ No newline at end of file
diff --git a/CringePlugins/Loader/PluginsLifetime.cs b/CringePlugins/Loader/PluginsLifetime.cs
index 9ce9472..21078f3 100644
--- a/CringePlugins/Loader/PluginsLifetime.cs
+++ b/CringePlugins/Loader/PluginsLifetime.cs
@@ -1,38 +1,71 @@
using System.Collections.Immutable;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using CringePlugins.Config;
+using CringePlugins.Resolver;
+using CringePlugins.Splash;
using NLog;
+using NuGet;
+using NuGet.Deps;
+using NuGet.Frameworks;
namespace CringePlugins.Loader;
-public class PluginsLifetime
+public class PluginsLifetime : ILoadingStage
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
- private readonly ImmutableArray _plugins;
+ public string Name => "Loading Plugins";
- public PluginsLifetime()
- {
- var dir = Directory.CreateDirectory(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CringeLauncher", "plugins"));
+ private ImmutableArray _plugins = [];
+ private readonly DirectoryInfo _dir = Directory.CreateDirectory(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CringeLauncher"));
+ private readonly NuGetFramework _runtimeFramework = NuGetFramework.ParseFolder("net8.0-windows");
- var plugins = ImmutableArray.Empty.ToBuilder();
+ public async ValueTask Load(ISplashProgress progress)
+ {
+ progress.DefineStepsCount(6);
- foreach (var directory in dir.EnumerateDirectories())
+ progress.Report("Discovering local plugins");
+
+ DiscoverLocalPlugins(_dir.CreateSubdirectory("plugins"));
+
+ progress.Report("Loading config");
+
+ PackagesConfig? packagesConfig = null;
+ var configPath = Path.Join(_dir.FullName, "packages.json");
+ if (File.Exists(configPath))
+ await using (var stream = File.OpenRead(configPath))
+ packagesConfig = JsonSerializer.Deserialize(stream, NuGetClient.SerializerOptions)!;
+
+ if (packagesConfig == null)
{
- var files = directory.GetFiles("*.dll");
-
- if (files.Length != 1) continue;
-
- try
- {
- plugins.Add(new PluginInstance(files[0].FullName));
- }
- catch (Exception e)
- {
- Log.Error(e, "Failed to load plugin {PluginPath}", files[0].FullName);
- }
+ packagesConfig = PackagesConfig.Default;
+ await using var stream = File.Create(configPath);
+ await JsonSerializer.SerializeAsync(stream, packagesConfig, NuGetClient.SerializerOptions);
}
- _plugins = plugins.ToImmutable();
+ progress.Report("Resolving packages");
+
+ var sourceMapping = new PackageSourceMapping(packagesConfig.Sources);
+ var resolver = new PackageResolver(_runtimeFramework, packagesConfig.Packages, sourceMapping);
+
+ var packages = await resolver.ResolveAsync();
+ progress.Report("Downloading packages");
+
+ var cachedPackages = await resolver.DownloadPackagesAsync(_dir.CreateSubdirectory("cache"), packages, progress);
+
+ progress.Report("Loading plugins");
+
+ await LoadPlugins(cachedPackages, sourceMapping);
+
+ progress.Report("Registering plugins");
+
+ RegisterLifetime();
+ }
+
+ private void RegisterLifetime()
+ {
foreach (var instance in _plugins)
{
try
@@ -46,4 +79,56 @@ public class PluginsLifetime
}
}
}
+
+ private async Task LoadPlugins(IReadOnlySet packages, PackageSourceMapping sourceMapping)
+ {
+ var plugins = _plugins.ToBuilder();
+
+ foreach (var package in packages)
+ {
+ var dir = Path.Join(package.Directory.FullName, "lib", package.ResolvedFramework.GetShortFolderName());
+
+ await using (var stream = File.Create(Path.Join(dir, $"{package.Package.Id}.deps.json")))
+ await DependencyManifestUtility.WriteDependencyManifestAsync(stream, package.Entry, _runtimeFramework,
+ sourceMapping,
+ dependency =>
+ packages.First(b => b.Package.Id.Equals(dependency.Id, StringComparison.OrdinalIgnoreCase))
+ .Package
+ .Version);
+
+ LoadComponent(plugins, Path.Join(dir, $"{package.Package.Id}.dll"));
+ }
+
+ _plugins = plugins.ToImmutable();
+ }
+
+ private void DiscoverLocalPlugins(DirectoryInfo dir)
+ {
+ var plugins = ImmutableArray.Empty.ToBuilder();
+
+ foreach (var directory in dir.EnumerateDirectories())
+ {
+ var files = directory.GetFiles("*.deps.json");
+
+ if (files.Length != 1) continue;
+
+ var path = files[0].FullName[..^".deps.json".Length] + ".dll";
+
+ LoadComponent(plugins, path);
+ }
+
+ _plugins = plugins.ToImmutable();
+ }
+
+ private static void LoadComponent(ImmutableArray.Builder plugins, string path)
+ {
+ try
+ {
+ plugins.Add(new PluginInstance(path));
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Failed to load plugin {PluginPath}", path);
+ }
+ }
}
\ No newline at end of file
diff --git a/CringePlugins/Render/RenderHandler.cs b/CringePlugins/Render/RenderHandler.cs
index 1406144..13f6d7c 100644
--- a/CringePlugins/Render/RenderHandler.cs
+++ b/CringePlugins/Render/RenderHandler.cs
@@ -4,7 +4,7 @@ using NLog;
namespace CringePlugins.Render;
-public sealed class RenderHandler : IDisposable
+public sealed class RenderHandler : IRootRenderComponent
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@@ -23,7 +23,7 @@ public sealed class RenderHandler : IDisposable
_components.Add(new ComponentRegistration(typeof(TComponent), instance));
}
- internal void OnFrame()
+ void IRenderComponent.OnFrame()
{
foreach (var (instanceType, renderComponent) in _components)
{
diff --git a/CringePlugins/Resolver/PackageResolver.cs b/CringePlugins/Resolver/PackageResolver.cs
new file mode 100644
index 0000000..fbb2043
--- /dev/null
+++ b/CringePlugins/Resolver/PackageResolver.cs
@@ -0,0 +1,148 @@
+using System.Collections.Immutable;
+using System.IO.Compression;
+using NuGet;
+using NuGet.Frameworks;
+using NuGet.Models;
+using NuGet.Versioning;
+
+namespace CringePlugins.Resolver;
+
+public class PackageResolver(NuGetFramework runtimeFramework, ImmutableArray references, PackageSourceMapping packageSources)
+{
+ public async Task> ResolveAsync()
+ {
+ var order = 0;
+ var packages = new SortedSet();
+
+ foreach (var reference in references)
+ {
+ var client = await packageSources.GetClientAsync(reference.Id);
+
+ var registrationRoot = await client.GetPackageRegistrationRootAsync(reference.Id);
+
+ NuGetVersion? version = null;
+ foreach (var page in registrationRoot.Items)
+ {
+ version = reference.Range.FindBestMatch(page.Items.Where(b => b.CatalogEntry.PackageTypes is [{ Name: "CringePlugin" }]).Select(b => b.CatalogEntry.Version));
+ if (version is not null)
+ break;
+ }
+
+ if (version is null)
+ throw new Exception($"Unable to find version for package {reference.Id}");
+
+ var package = new Package(order, reference.Id, version, []); // todo resolve dependencies
+
+ if (packages.Add(package))
+ continue;
+
+ if (!packages.TryGetValue(package, out var existingPackage))
+ throw new Exception($"Duplicate package {package.Id}");
+
+ if (package.Version < existingPackage.Version)
+ throw new Exception($"Package reference {package.Id} has lower version {package.Version} than already resolved {existingPackage.Version}");
+
+ if (package.Version == existingPackage.Version)
+ continue;
+
+ packages.Remove(existingPackage);
+ packages.Add(package with
+ {
+ Order = ++order
+ });
+ }
+
+ var set = ImmutableHashSet.Empty.ToBuilder();
+ foreach (var package in packages)
+ {
+ var client = await packageSources.GetClientAsync(package.Id);
+
+ var (catalogEntryUrl, catalogEntry) = await client.GetPackageRegistrationAsync(package.Id, package.Version);
+
+ catalogEntry ??= await client.GetPackageCatalogEntryAsync(catalogEntryUrl);
+
+ var nearestGroup = NuGetFrameworkUtility.GetNearest(catalogEntry.DependencyGroups, runtimeFramework,
+ g => g.TargetFramework);
+
+ if (nearestGroup is null)
+ throw new Exception($"Unable to find compatible dependency group for package {package.Id}");
+
+ set.Add(new RemotePackage(package, nearestGroup.TargetFramework, client, catalogEntry));
+ }
+
+ return set.ToImmutable();
+ }
+
+ public async Task> DownloadPackagesAsync(DirectoryInfo baseDirectory,
+ IReadOnlySet resolvedPackages, IProgress? progress = null)
+ {
+ var packages = ImmutableHashSet.Empty.ToBuilder();
+
+ var i = 0f;
+ foreach (var package in resolvedPackages)
+ {
+ switch (package)
+ {
+ case RemotePackage remotePackage:
+ {
+ var dir = new DirectoryInfo(Path.Join(baseDirectory.FullName, package.Package.Id, package.Package.Version.ToString()));
+ if (!dir.Exists)
+ {
+ dir.Create();
+
+ await using var stream = await remotePackage.Client.GetPackageContentStreamAsync(remotePackage.Package.Id, remotePackage.Package.Version);
+ using var memStream = new MemoryStream();
+ await stream.CopyToAsync(memStream);
+ memStream.Position = 0;
+ using var archive = new ZipArchive(memStream, ZipArchiveMode.Read);
+ archive.ExtractToDirectory(dir.FullName);
+ }
+
+ packages.Add(new CachedPackage(package.Package, package.ResolvedFramework, dir, package.Entry));
+ break;
+ }
+ case CachedPackage cachedPackage:
+ packages.Add(cachedPackage);
+ break;
+ }
+
+ progress?.Report(i++ / resolvedPackages.Count);
+ }
+
+ return packages.ToImmutable();
+ }
+}
+
+public record CachedPackage(Package Package, NuGetFramework ResolvedFramework, DirectoryInfo Directory, CatalogEntry Entry) : ResolvedPackage(Package, ResolvedFramework, Entry);
+public record RemotePackage(Package Package, NuGetFramework ResolvedFramework, NuGetClient Client, CatalogEntry Entry) : ResolvedPackage(Package, ResolvedFramework, Entry);
+
+public abstract record ResolvedPackage(Package Package, NuGetFramework ResolvedFramework, CatalogEntry Entry);
+
+public record Package(int Order, string Id, NuGetVersion Version, ImmutableArray Dependencies) : IComparable, IComparable
+{
+ public int CompareTo(Package? other)
+ {
+ if (ReferenceEquals(this, other)) return 0;
+ if (other is null) return 1;
+ var orderComparison = Order.CompareTo(other.Order);
+ if (orderComparison != 0) return orderComparison;
+ return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public int CompareTo(object? obj)
+ {
+ if (obj is null) return 1;
+ if (ReferenceEquals(this, obj)) return 0;
+ return obj is Package other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(Package)}");
+ }
+
+ public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Id);
+
+ public virtual bool Equals(Package? other)
+ {
+ if (other is null) return false;
+ return Id.Equals(other.Id, StringComparison.OrdinalIgnoreCase);
+ }
+}
+
+public record PackageReference(string Id, VersionRange Range);
\ No newline at end of file
diff --git a/CringePlugins/Splash/ISplashProgress.cs b/CringePlugins/Splash/ISplashProgress.cs
new file mode 100644
index 0000000..5951f19
--- /dev/null
+++ b/CringePlugins/Splash/ISplashProgress.cs
@@ -0,0 +1,13 @@
+namespace CringePlugins.Splash;
+
+public interface ISplashProgress : IProgress, IProgress
+{
+ void DefineStage(ILoadingStage stage);
+ void DefineStepsCount(int count);
+}
+
+public interface ILoadingStage
+{
+ string Name { get; }
+ ValueTask Load(ISplashProgress progress);
+}
\ No newline at end of file
diff --git a/CringePlugins/Splash/ProgressInfo.cs b/CringePlugins/Splash/ProgressInfo.cs
new file mode 100644
index 0000000..891432d
--- /dev/null
+++ b/CringePlugins/Splash/ProgressInfo.cs
@@ -0,0 +1,9 @@
+namespace CringePlugins.Splash;
+
+public record ProgressInfo(string Text)
+{
+ public static implicit operator ProgressInfo(string s) => new(s);
+ public static implicit operator ProgressInfo((string, float) s) => new PercentProgressInfo(s.Item1, s.Item2);
+}
+
+public record PercentProgressInfo(string Text, float Percent = 0) : ProgressInfo(Text);
\ No newline at end of file
diff --git a/CringePlugins/Splash/Splash.cs b/CringePlugins/Splash/Splash.cs
new file mode 100644
index 0000000..5b13456
--- /dev/null
+++ b/CringePlugins/Splash/Splash.cs
@@ -0,0 +1,29 @@
+namespace CringePlugins.Splash;
+
+public class Splash : ISplashProgress
+{
+ private readonly List _loadingStages = [];
+
+ public void Report(ProgressInfo value)
+ {
+ }
+
+ public void Report(float value)
+ {
+ }
+
+ public void DefineStage(ILoadingStage stage) => _loadingStages.Add(stage);
+
+ public void DefineStepsCount(int count)
+ {
+ }
+
+ public void ExecuteLoadingStages()
+ {
+ foreach (var loadingStage in _loadingStages)
+ {
+ // todo sync context
+ loadingStage.Load(this).AsTask().GetAwaiter().GetResult();
+ }
+ }
+}
\ No newline at end of file
diff --git a/CringePlugins/packages.lock.json b/CringePlugins/packages.lock.json
index 23ff5bf..18bfc61 100644
--- a/CringePlugins/packages.lock.json
+++ b/CringePlugins/packages.lock.json
@@ -1,7 +1,7 @@
{
"version": 1,
"dependencies": {
- "net8.0": {
+ "net8.0-windows7.0": {
"dnlib": {
"type": "Direct",
"requested": "[4.4.0, )",
@@ -23,22 +23,22 @@
"SpaceEngineersDedicated.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.*, )",
- "resolved": "1.204.18",
- "contentHash": "GT7/9CBMx4jjor41zLOOl87YYM/JdJD8xp9ccXyuhP2oUaz25H3ZmCQuGeAuZNENKru1a/7hZrId4PwlMDGoew==",
+ "resolved": "1.205.23",
+ "contentHash": "J7mF5hY39PzzCZps6vhIRzKiq8vD6Af9TgumTJR068vjEi+BzyeEFhqX+cl2Dd1ngOmsBtGWc5m+vxgTfs5YuA==",
"dependencies": {
"SharpDX": "4.2.0-keen-cringe",
"protobuf-net": "1.0.0"
}
},
- "ImGui.NET.DirectX": {
+ "NuGet.Frameworks": {
"type": "Transitive",
- "resolved": "1.91.0.1",
- "contentHash": "PpW1gQ9g97h6Hm/h/tkSBOmsBYgGwN8wKNmlJomcQFD/zRY1HPkJZz18XRSfRLHPmH2eeh4hhhZv1KHug7dF9g==",
- "dependencies": {
- "System.Buffers": "4.5.1",
- "System.Numerics.Vectors": "4.5.0",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
+ "resolved": "6.11.1",
+ "contentHash": "plTZ3ariSWQVsFn2mk83SsdmSg1VpgIMTSZpP/eSE/NNQF02p+M9ItxAYeUZBMX+cQ2nFkSwxQRJ0/fkaV9Hbg=="
+ },
+ "NuGet.Versioning": {
+ "type": "Transitive",
+ "resolved": "6.11.1",
+ "contentHash": "YNn3BB71F+guJW42TbAhGcMh3gpyqFMZcPVD9pm5vcvGivTALtRely/VCPWQQ6JQ5PfwIrjPaJMO7VnqyeK3rg=="
},
"protobuf-net": {
"type": "Transitive",
@@ -50,28 +50,21 @@
"resolved": "4.2.0-keen-cringe",
"contentHash": "LaJN3h1Gi1FWVdef2I5WtOH9gwzKCBniH0CragarbkN2QheYY6Lqm+91PcOfp1w/4wdVb+k8Kjv3sO393Tphtw=="
},
- "System.Buffers": {
- "type": "Transitive",
- "resolved": "4.5.1",
- "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
- },
- "System.Numerics.Vectors": {
- "type": "Transitive",
- "resolved": "4.5.0",
- "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
- },
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
- },
"cringebootstrap.abstractions": {
"type": "Project"
},
- "cringerender": {
+ "nuget": {
"type": "Project",
"dependencies": {
- "ImGui.NET.DirectX": "[1.91.0.1, )",
+ "NuGet.Frameworks": "[6.11.1, )",
+ "NuGet.Versioning": "[6.11.1, )"
+ }
+ },
+ "sharedcringe": {
+ "type": "Project",
+ "dependencies": {
+ "CringeBootstrap.Abstractions": "[1.0.0, )",
+ "NLog": "[5.3.4, )",
"SpaceEngineersDedicated.ReferenceAssemblies": "[1.*, )"
}
}
diff --git a/CringePlugins/splash.gif b/CringePlugins/splash.gif
new file mode 100644
index 0000000..ad6fb35
Binary files /dev/null and b/CringePlugins/splash.gif differ
diff --git a/NuGet/Converters/FrameworkJsonConverter.cs b/NuGet/Converters/FrameworkJsonConverter.cs
new file mode 100644
index 0000000..0327247
--- /dev/null
+++ b/NuGet/Converters/FrameworkJsonConverter.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Frameworks;
+
+namespace NuGet.Converters;
+
+public class FrameworkJsonConverter(FrameworkNameFormat format) : JsonConverter
+{
+ public override NuGetFramework? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("Invalid framework string");
+
+ var s = reader.GetString()!;
+ return format switch
+ {
+ FrameworkNameFormat.ShortFolderName => NuGetFramework.ParseFolder(s),
+ FrameworkNameFormat.FrameworkName => NuGetFramework.ParseFrameworkName(s,
+ DefaultFrameworkNameProvider.Instance),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, NuGetFramework value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(format switch
+ {
+ FrameworkNameFormat.ShortFolderName => value.GetShortFolderName(),
+ FrameworkNameFormat.FrameworkName => value.DotNetFrameworkName,
+ _ => throw new ArgumentOutOfRangeException()
+ });
+ }
+}
+
+public enum FrameworkNameFormat
+{
+ ///
+ /// The short folder name format (net8.0)
+ ///
+ ShortFolderName,
+ ///
+ /// Full framework name (.NETCoreApp,Version=v8.0)
+ ///
+ FrameworkName
+}
\ No newline at end of file
diff --git a/NuGet/Converters/ManifestPackageKeyJsonConverter.cs b/NuGet/Converters/ManifestPackageKeyJsonConverter.cs
new file mode 100644
index 0000000..fb99a0d
--- /dev/null
+++ b/NuGet/Converters/ManifestPackageKeyJsonConverter.cs
@@ -0,0 +1,27 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Deps;
+
+namespace NuGet.Converters;
+
+public class ManifestPackageKeyJsonConverter : JsonConverter
+{
+ public override ManifestPackageKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("Invalid package key string");
+
+ return ManifestPackageKey.Parse(reader.GetString()!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, ManifestPackageKey value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+
+ public override ManifestPackageKey ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert,
+ JsonSerializerOptions options) => Read(ref reader, typeToConvert, options);
+
+ public override void WriteAsPropertyName(Utf8JsonWriter writer, ManifestPackageKey value,
+ JsonSerializerOptions options) => Write(writer, value, options);
+}
\ No newline at end of file
diff --git a/NuGet/Converters/ResourceTypeJsonConverter.cs b/NuGet/Converters/ResourceTypeJsonConverter.cs
new file mode 100644
index 0000000..edb3929
--- /dev/null
+++ b/NuGet/Converters/ResourceTypeJsonConverter.cs
@@ -0,0 +1,21 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Models;
+
+namespace NuGet.Converters;
+
+public class ResourceTypeJsonConverter : JsonConverter
+{
+ public override ResourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("Invalid resource type");
+
+ return ResourceType.Parse(reader.GetString()!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, ResourceType value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+}
\ No newline at end of file
diff --git a/NuGet/Converters/VersionJsonConverter.cs b/NuGet/Converters/VersionJsonConverter.cs
new file mode 100644
index 0000000..4addc75
--- /dev/null
+++ b/NuGet/Converters/VersionJsonConverter.cs
@@ -0,0 +1,21 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Versioning;
+
+namespace NuGet.Converters;
+
+public class VersionJsonConverter : JsonConverter
+{
+ public override NuGetVersion? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("Invalid version string");
+
+ return NuGetVersion.Parse(reader.GetString()!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, NuGetVersion value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+}
\ No newline at end of file
diff --git a/NuGet/Converters/VersionRangeJsonConverter.cs b/NuGet/Converters/VersionRangeJsonConverter.cs
new file mode 100644
index 0000000..b15c898
--- /dev/null
+++ b/NuGet/Converters/VersionRangeJsonConverter.cs
@@ -0,0 +1,21 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Versioning;
+
+namespace NuGet.Converters;
+
+public class VersionRangeJsonConverter : JsonConverter
+{
+ public override VersionRange? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("Invalid version range");
+
+ return VersionRange.Parse(reader.GetString()!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, VersionRange value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+}
\ No newline at end of file
diff --git a/NuGet/Deps/DependenciesManifest.cs b/NuGet/Deps/DependenciesManifest.cs
new file mode 100644
index 0000000..c74d0c9
--- /dev/null
+++ b/NuGet/Deps/DependenciesManifest.cs
@@ -0,0 +1,123 @@
+using System.Collections.Immutable;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Converters;
+using NuGet.Frameworks;
+using NuGet.Models;
+using NuGet.Versioning;
+
+namespace NuGet.Deps;
+
+public record DependenciesManifest(RuntimeTarget RuntimeTarget,
+ ImmutableDictionary CompilationOptions,
+ ImmutableDictionary> Targets,
+ ImmutableDictionary Libraries);
+
+public record DependencyLibrary(
+ LibraryType Type,
+ string Sha512 = "",
+ bool Serviceable = false,
+ ManifestPackageKey? Path = null,
+ string? HashPath = null);
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum LibraryType
+{
+ Project,
+ Package
+}
+
+public record DependencyTarget(ImmutableDictionary? Dependencies,
+ // key is file path relative to package root
+ ImmutableDictionary? Runtime,
+ // key is file path relative to package root
+ ImmutableDictionary? Native);
+
+public record Dependency(Version? FileVersion = null);
+
+public record RuntimeDependency(Version? AssemblyVersion = null, Version? FileVersion = null) : Dependency(FileVersion);
+
+public record RuntimeTarget([property: JsonPropertyName("name")] NuGetFramework Framework, string Signature = "");
+
+[JsonConverter(typeof(ManifestPackageKeyJsonConverter))]
+public record ManifestPackageKey(string Id, NuGetVersion Version)
+{
+ public static ManifestPackageKey Parse(string str)
+ {
+ var index = str.IndexOf('/');
+ if (index < 0)
+ throw new FormatException("Invalid package key: " + str);
+
+ return new ManifestPackageKey(str[..index], NuGetVersion.Parse(str[(index + 1)..]));
+ }
+
+ public override string ToString() => $"{Id}/{Version}";
+}
+
+public static class DependencyManifestUtility
+{
+ private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = true,
+ Converters =
+ {
+ new FrameworkJsonConverter(FrameworkNameFormat.FrameworkName),
+ new VersionJsonConverter()
+ }
+ };
+
+ public static async ValueTask WriteDependencyManifestAsync(Stream stream, CatalogEntry catalogEntry, NuGetFramework targetFramework,
+ PackageSourceMapping packageSources, Func versionResolver)
+ {
+ var runtimeTarget = new RuntimeTarget(targetFramework);
+
+ var targets = ImmutableDictionary.Empty.ToBuilder();
+
+ await MapCatalogEntry(catalogEntry, targetFramework, packageSources, versionResolver, targets);
+
+ var manifest = new DependenciesManifest(runtimeTarget, ImmutableDictionary.Empty,
+ ImmutableDictionary>.Empty
+ .Add(targetFramework, targets.ToImmutable()),
+ ImmutableDictionary.Empty);
+
+ await JsonSerializer.SerializeAsync(stream, manifest, SerializerOptions);
+ }
+
+ private static async Task MapCatalogEntry(CatalogEntry catalogEntry, NuGetFramework targetFramework,
+ PackageSourceMapping packageSources, Func versionResolver, ImmutableDictionary.Builder targets)
+ {
+ if (targets.ContainsKey(new(catalogEntry.Id, catalogEntry.Version)))
+ return;
+
+ var nearest = NuGetFrameworkUtility.GetNearest(catalogEntry.DependencyGroups, targetFramework,
+ group => group.TargetFramework);
+
+ if (nearest is null)
+ return;
+
+ targets.Add(new(catalogEntry.Id, catalogEntry.Version),
+ MapEntry(catalogEntry, nearest, targetFramework, versionResolver));
+
+ foreach (var dependency in nearest.Dependencies)
+ {
+ var client = await packageSources.GetClientAsync(dependency.Id);
+ var (url, entry) = await client.GetPackageRegistrationAsync(dependency.Id, versionResolver(dependency));
+
+ entry ??= await client.GetPackageCatalogEntryAsync(url);
+
+ await MapCatalogEntry(entry, targetFramework, packageSources, versionResolver, targets);
+ }
+ }
+
+ private static DependencyTarget MapEntry(CatalogEntry entry, DependencyGroup group, NuGetFramework targetFramework, Func versionResolver)
+ {
+ return new(group.Dependencies.ToImmutableDictionary(b => b.Id, versionResolver),
+ entry.PackageEntries.Where(b => b.FullName.StartsWith($"lib/{targetFramework.GetShortFolderName()}/"))
+ .ToImmutableDictionary(b => b.FullName, _ => new RuntimeDependency()),
+ entry.PackageEntries.Where(b =>
+ b.FullName.StartsWith($"runtimes/{RuntimeInformation.RuntimeIdentifier}/native/"))
+ .ToImmutableDictionary(b => b.FullName, _ => new Dependency()));
+ }
+}
\ No newline at end of file
diff --git a/NuGet/Models/CatalogEntry.cs b/NuGet/Models/CatalogEntry.cs
new file mode 100644
index 0000000..b943755
--- /dev/null
+++ b/NuGet/Models/CatalogEntry.cs
@@ -0,0 +1,9 @@
+using NuGet.Versioning;
+
+namespace NuGet.Models;
+
+public record CatalogEntry(string Id, NuGetVersion Version, DependencyGroup[] DependencyGroups, PackageType[]? PackageTypes,
+ CatalogPackageEntry[] PackageEntries,
+ bool Serviceable = true);
+
+public record CatalogPackageEntry(string Name, string FullName, long CompressedLength, long Length);
\ No newline at end of file
diff --git a/NuGet/Models/Dependency.cs b/NuGet/Models/Dependency.cs
new file mode 100644
index 0000000..e1bec5a
--- /dev/null
+++ b/NuGet/Models/Dependency.cs
@@ -0,0 +1,5 @@
+using NuGet.Versioning;
+
+namespace NuGet.Models;
+
+public record Dependency(string Id, VersionRange Range);
\ No newline at end of file
diff --git a/NuGet/Models/DependencyGroup.cs b/NuGet/Models/DependencyGroup.cs
new file mode 100644
index 0000000..4c7a3b5
--- /dev/null
+++ b/NuGet/Models/DependencyGroup.cs
@@ -0,0 +1,5 @@
+using NuGet.Frameworks;
+
+namespace NuGet.Models;
+
+public record DependencyGroup(NuGetFramework TargetFramework, Dependency[] Dependencies);
\ No newline at end of file
diff --git a/NuGet/Models/NuGetIndex.cs b/NuGet/Models/NuGetIndex.cs
new file mode 100644
index 0000000..057880d
--- /dev/null
+++ b/NuGet/Models/NuGetIndex.cs
@@ -0,0 +1,5 @@
+using NuGet.Versioning;
+
+namespace NuGet.Models;
+
+public record NuGetIndex(NuGetVersion Version, Resource[] Resources);
\ No newline at end of file
diff --git a/NuGet/Models/PackageType.cs b/NuGet/Models/PackageType.cs
new file mode 100644
index 0000000..b1d767f
--- /dev/null
+++ b/NuGet/Models/PackageType.cs
@@ -0,0 +1,3 @@
+namespace NuGet.Models;
+
+public record PackageType(string Name);
\ No newline at end of file
diff --git a/NuGet/Models/Registration.cs b/NuGet/Models/Registration.cs
new file mode 100644
index 0000000..3e1ce58
--- /dev/null
+++ b/NuGet/Models/Registration.cs
@@ -0,0 +1,6 @@
+using System.Text.Json.Serialization;
+
+namespace NuGet.Models;
+
+public record Registration([property: JsonPropertyName("catalogEntry")] string CatalogEntryUrl,
+ [property: JsonPropertyName("sleet:catalogEntry")] CatalogEntry? SleetEntry);
\ No newline at end of file
diff --git a/NuGet/Models/RegistrationEntry.cs b/NuGet/Models/RegistrationEntry.cs
new file mode 100644
index 0000000..f704314
--- /dev/null
+++ b/NuGet/Models/RegistrationEntry.cs
@@ -0,0 +1,3 @@
+namespace NuGet.Models;
+
+public record RegistrationEntry(CatalogEntry CatalogEntry);
\ No newline at end of file
diff --git a/NuGet/Models/RegistrationPage.cs b/NuGet/Models/RegistrationPage.cs
new file mode 100644
index 0000000..4626929
--- /dev/null
+++ b/NuGet/Models/RegistrationPage.cs
@@ -0,0 +1,5 @@
+using NuGet.Versioning;
+
+namespace NuGet.Models;
+
+public record RegistrationPage(int Count, NuGetVersion Lower, NuGetVersion Upper, RegistrationEntry[] Items);
\ No newline at end of file
diff --git a/NuGet/Models/RegistrationRoot.cs b/NuGet/Models/RegistrationRoot.cs
new file mode 100644
index 0000000..dd7ff30
--- /dev/null
+++ b/NuGet/Models/RegistrationRoot.cs
@@ -0,0 +1,3 @@
+namespace NuGet.Models;
+
+public record RegistrationRoot(int Count, RegistrationPage[] Items);
\ No newline at end of file
diff --git a/NuGet/Models/Resource.cs b/NuGet/Models/Resource.cs
new file mode 100644
index 0000000..6d57df4
--- /dev/null
+++ b/NuGet/Models/Resource.cs
@@ -0,0 +1,5 @@
+using System.Text.Json.Serialization;
+
+namespace NuGet.Models;
+
+public record Resource([property: JsonPropertyName("@id")] string Url, [property: JsonPropertyName("@type")] ResourceType Type, string? Comment);
\ No newline at end of file
diff --git a/NuGet/Models/ResourceType.cs b/NuGet/Models/ResourceType.cs
new file mode 100644
index 0000000..fc3c8cc
--- /dev/null
+++ b/NuGet/Models/ResourceType.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+using NuGet.Converters;
+using NuGet.Versioning;
+
+namespace NuGet.Models;
+
+[JsonConverter(typeof(ResourceTypeJsonConverter))]
+public record ResourceType(string Id, NuGetVersion? Version)
+{
+ public static ResourceType Parse(string typeString)
+ {
+ var slash = typeString.IndexOf('/');
+
+ if (slash < 0)
+ return new ResourceType(typeString, null);
+
+ var id = typeString[..slash];
+ var versionStr = typeString[(slash + 1)..];
+
+ return NuGetVersion.TryParse(versionStr, out var version)
+ ? new ResourceType(id, version)
+ : new ResourceType(id, null);
+ }
+
+ public override string ToString() => $"{Id}/{Version}";
+}
\ No newline at end of file
diff --git a/NuGet/NuGet.csproj b/NuGet/NuGet.csproj
new file mode 100644
index 0000000..5e23092
--- /dev/null
+++ b/NuGet/NuGet.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/NuGet/NuGetClient.cs b/NuGet/NuGetClient.cs
new file mode 100644
index 0000000..c7c30dd
--- /dev/null
+++ b/NuGet/NuGetClient.cs
@@ -0,0 +1,73 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using NuGet.Converters;
+using NuGet.Models;
+using NuGet.Versioning;
+
+namespace NuGet;
+
+public class NuGetClient
+{
+ private readonly HttpClient _client;
+ private readonly Uri _packageBaseAddress;
+ private readonly Uri _registration;
+
+ public static JsonSerializerOptions SerializerOptions { get; } = new(JsonSerializerDefaults.Web)
+ {
+ Converters =
+ {
+ new VersionJsonConverter(),
+ new VersionRangeJsonConverter(),
+ new FrameworkJsonConverter(FrameworkNameFormat.ShortFolderName),
+ },
+ WriteIndented = true
+ };
+
+ private NuGetClient(HttpClient client, Uri packageBaseAddress, Uri registration)
+ {
+ _client = client;
+ _packageBaseAddress = packageBaseAddress;
+ _registration = registration;
+ }
+
+ public Task GetPackageContentStreamAsync(string id, NuGetVersion version)
+ {
+ return _client.GetStreamAsync(new Uri(_packageBaseAddress,
+ new Uri($"{id.ToLower()}/{version}.nupkg", UriKind.Relative)));
+ }
+
+ public Task GetPackageRegistrationAsync(string id, NuGetVersion version)
+ {
+ return _client.GetFromJsonAsync(
+ new Uri(_registration,
+ new Uri($"{id.ToLower()}/{version}.json", UriKind.Relative)),
+ SerializerOptions
+ )!;
+ }
+
+ public Task GetPackageRegistrationRootAsync(string id)
+ {
+ return _client.GetFromJsonAsync(
+ new Uri(_registration,
+ new Uri($"{id.ToLower()}/index.json", UriKind.Relative)),
+ SerializerOptions
+ )!;
+ }
+
+ public Task GetPackageCatalogEntryAsync(string url)
+ {
+ return _client.GetFromJsonAsync(url, SerializerOptions)!;
+ }
+
+ public static async Task CreateFromIndexUrlAsync(string indexUrl)
+ {
+ var client = new HttpClient();
+
+ var index = await client.GetFromJsonAsync(indexUrl, SerializerOptions);
+
+ var (packageBaseAddress, _, _) = index!.Resources.First(b => b.Type.Id == "PackageBaseAddress");
+ var (registration, _, _) = index!.Resources.First(b => b.Type.Id == "RegistrationsBaseUrl");
+
+ return new NuGetClient(client, new Uri(packageBaseAddress), new Uri(registration));
+ }
+}
\ No newline at end of file
diff --git a/NuGet/PackageSourceMapping.cs b/NuGet/PackageSourceMapping.cs
new file mode 100644
index 0000000..5cda169
--- /dev/null
+++ b/NuGet/PackageSourceMapping.cs
@@ -0,0 +1,16 @@
+using System.Collections.Immutable;
+
+namespace NuGet;
+
+public class PackageSourceMapping(ImmutableArray sources)
+{
+ private readonly ImmutableDictionary> _clients = sources.Select(b =>
+ new KeyValuePair>(b.Pattern,
+ NuGetClient.CreateFromIndexUrlAsync(b.Url)))
+ .ToImmutableDictionary();
+
+ public Task GetClientAsync(string packageId) =>
+ _clients.FirstOrDefault(b => packageId.StartsWith(b.Key)).Value;
+}
+
+public record PackageSource(string Pattern, string Url);
\ No newline at end of file
diff --git a/SharedCringe/Loader/DerivedAssemblyLoadContext.cs b/SharedCringe/Loader/DerivedAssemblyLoadContext.cs
new file mode 100644
index 0000000..ffa7990
--- /dev/null
+++ b/SharedCringe/Loader/DerivedAssemblyLoadContext.cs
@@ -0,0 +1,14 @@
+using System.Reflection;
+using System.Runtime.Loader;
+using CringeBootstrap.Abstractions;
+
+namespace SharedCringe.Loader;
+
+public abstract class DerivedAssemblyLoadContext(ICoreLoadContext parentContext, string name)
+ : AssemblyLoadContext(name, true)
+{
+ protected readonly ICoreLoadContext ParentContext = parentContext;
+
+ protected override Assembly? Load(AssemblyName assemblyName) => ParentContext.ResolveFromAssemblyName(assemblyName);
+ protected override nint LoadUnmanagedDll(string unmanagedDllName) => ParentContext.ResolveUnmanagedDll(unmanagedDllName);
+}
\ No newline at end of file
diff --git a/SharedCringe/SharedCringe.csproj b/SharedCringe/SharedCringe.csproj
new file mode 100644
index 0000000..bbbbf75
--- /dev/null
+++ b/SharedCringe/SharedCringe.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SharedCringe/packages.lock.json b/SharedCringe/packages.lock.json
new file mode 100644
index 0000000..3a1beba
--- /dev/null
+++ b/SharedCringe/packages.lock.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "NLog": {
+ "type": "Direct",
+ "requested": "[5.3.4, )",
+ "resolved": "5.3.4",
+ "contentHash": "gLy7+O1hEYJXIlcTr1/VWjGXrZTQFZzYNO18IWasD64pNwz0BreV+nHLxWKXWZzERRzoKnsk2XYtwLkTVk7J1A=="
+ },
+ "SpaceEngineersDedicated.ReferenceAssemblies": {
+ "type": "Direct",
+ "requested": "[1.*, )",
+ "resolved": "1.205.23",
+ "contentHash": "J7mF5hY39PzzCZps6vhIRzKiq8vD6Af9TgumTJR068vjEi+BzyeEFhqX+cl2Dd1ngOmsBtGWc5m+vxgTfs5YuA==",
+ "dependencies": {
+ "SharpDX": "4.2.0-keen-cringe",
+ "protobuf-net": "1.0.0"
+ }
+ },
+ "protobuf-net": {
+ "type": "Transitive",
+ "resolved": "1.0.0",
+ "contentHash": "kTGOK0E87473sOImOjgZOnz3kTC2aMLffoRWQLYNuBLJnwNNmjanF9IkevZ9Q7yYLeABQfcF3BpeepuMntMVNw=="
+ },
+ "SharpDX": {
+ "type": "Transitive",
+ "resolved": "4.2.0-keen-cringe",
+ "contentHash": "LaJN3h1Gi1FWVdef2I5WtOH9gwzKCBniH0CragarbkN2QheYY6Lqm+91PcOfp1w/4wdVb+k8Kjv3sO393Tphtw=="
+ },
+ "cringebootstrap.abstractions": {
+ "type": "Project"
+ }
+ }
+ }
+}
\ No newline at end of file