Support reloading local plugins
All checks were successful
Build / Compute Version (push) Successful in 6s
Build / Build Nuget package (NuGet) (push) Successful in 4m35s
Build / Build Nuget package (SharedCringe) (push) Successful in 4m44s
Build / Build Nuget package (CringeBootstrap.Abstractions) (push) Successful in 4m51s
Build / Build Nuget package (CringePlugins) (push) Successful in 5m11s
Build / Build Launcher (push) Successful in 6m16s

Added drag and drop functionality to sources
This commit is contained in:
2025-06-07 11:54:12 -04:00
parent 94fc8a55c0
commit 60b8a94ab2
7 changed files with 191 additions and 21 deletions

View File

@@ -21,6 +21,11 @@
<Publicize Include="VRage:VRage.Plugins.MyPlugins.m_handleInputPlugins" /> <Publicize Include="VRage:VRage.Plugins.MyPlugins.m_handleInputPlugins" />
<Publicize Include="VRage.Render11:VRageRender.MyCommon" /> <Publicize Include="VRage.Render11:VRageRender.MyCommon" />
<Publicize Include="VRage.Render11:VRageRender.MyRender11.DeviceInstance" /> <Publicize Include="VRage.Render11:VRageRender.MyRender11.DeviceInstance" />
<Publicize Include="VRage.Game:VRage.Game.MyDefinitionManagerBase.m_registeredAssemblies" />
<Publicize Include="VRage.Game:VRage.Game.MyDefinitionManagerBase.m_registered" />
<Publicize Include="VRage.Game:VRage.Game.ObjectBuilder.MyGlobalTypeMetadata.m_assemblies" />
<Publicize Include="VRage:VRage.MyXmlSerializerManager.m_registeredAssemblies" />
<Publicize Include="Sandbox.Game:Sandbox.Game.World.MySession.m_sessionComponents" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,16 @@
using CringeBootstrap.Abstractions;
using System.Reflection;
namespace CringePlugins.Loader;
internal class LocalLoadContext(ICoreLoadContext parentContext, string entrypointPath) : PluginAssemblyLoadContext(parentContext, entrypointPath)
{
//use MemoryStream so the file can be written over, and check for .pdb
protected override Assembly LoadAssemblyFile(string path)
{
var pdbFile = Path.ChangeExtension(path, ".pdb");
return File.Exists(pdbFile)
? LoadFromStream(new MemoryStream(File.ReadAllBytes(path)), new MemoryStream(File.ReadAllBytes(pdbFile)))
: LoadFromStream(new MemoryStream(File.ReadAllBytes(path)));
}
}

View File

@@ -30,7 +30,7 @@ internal class PluginAssemblyLoadContext : DerivedAssemblyLoadContext
if (_assembly is not null) if (_assembly is not null)
return _assembly; return _assembly;
_assembly = LoadFromAssemblyPath(_entrypointPath); _assembly = LoadAssemblyFile(_entrypointPath);
var module = _assembly.GetMainModule(); var module = _assembly.GetMainModule();
@@ -50,7 +50,7 @@ internal class PluginAssemblyLoadContext : DerivedAssemblyLoadContext
protected override Assembly? Load(AssemblyName assemblyName) protected override Assembly? Load(AssemblyName assemblyName)
{ {
if (_dependencyResolver.ResolveAssemblyToPath(assemblyName) is { } path) if (_dependencyResolver.ResolveAssemblyToPath(assemblyName) is { } path)
return LoadFromAssemblyPath(path); return LoadAssemblyFile(path);
return base.Load(assemblyName); return base.Load(assemblyName);
} }
@@ -63,6 +63,8 @@ internal class PluginAssemblyLoadContext : DerivedAssemblyLoadContext
return base.LoadUnmanagedDll(unmanagedDllName); return base.LoadUnmanagedDll(unmanagedDllName);
} }
protected virtual Assembly LoadAssemblyFile(string path) => LoadFromAssemblyPath(path);
private static void OnUnload(AssemblyLoadContext context) private static void OnUnload(AssemblyLoadContext context)
{ {
if (context is not PluginAssemblyLoadContext pluginContext) if (context is not PluginAssemblyLoadContext pluginContext)

View File

@@ -3,17 +3,26 @@ using System.Runtime.Loader;
using CringeBootstrap.Abstractions; using CringeBootstrap.Abstractions;
using CringePlugins.Utils; using CringePlugins.Utils;
using NLog; using NLog;
using Sandbox;
using Sandbox.Game.World;
using SharedCringe.Loader; using SharedCringe.Loader;
using VRage;
using VRage.Game;
using VRage.Game.ObjectBuilder;
using VRage.Plugins; using VRage.Plugins;
namespace CringePlugins.Loader; namespace CringePlugins.Loader;
internal sealed class PluginInstance(PluginMetadata metadata, string entrypointPath) internal sealed class PluginInstance(PluginMetadata metadata, string entrypointPath, bool local)
{ {
public bool HasConfig => _openConfigAction != null; public bool HasConfig => _openConfigAction != null;
public bool IsReloading => _disposeTcs?.Task.IsCompleted == false;
public bool IsLocal => local;
private PluginAssemblyLoadContext? _context; private PluginAssemblyLoadContext? _context;
private IPlugin? _instance; private IPlugin? _instance;
private TaskCompletionSource<(DerivedAssemblyLoadContext OldContext, DerivedAssemblyLoadContext NewContext)>? _disposeTcs;
private Action? _openConfigAction; private Action? _openConfigAction;
public PluginWrapper? WrappedInstance { get; private set; } public PluginWrapper? WrappedInstance { get; private set; }
@@ -21,7 +30,7 @@ internal sealed class PluginInstance(PluginMetadata metadata, string entrypointP
private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
public PluginMetadata Metadata { get; } = metadata; public PluginMetadata Metadata { get; } = metadata;
public PluginInstance(string entrypointPath) : this(PluginMetadata.ReadFromEntrypoint(entrypointPath), entrypointPath) public PluginInstance(string entrypointPath, bool local) : this(PluginMetadata.ReadFromEntrypoint(entrypointPath), entrypointPath, local)
{ {
} }
@@ -30,7 +39,7 @@ internal sealed class PluginInstance(PluginMetadata metadata, string entrypointP
if (AssemblyLoadContext.GetLoadContext(typeof(PluginInstance).Assembly) is not ICoreLoadContext parentContext) if (AssemblyLoadContext.GetLoadContext(typeof(PluginInstance).Assembly) is not ICoreLoadContext parentContext)
throw new NotSupportedException("Plugin instantiation is not supported in this context"); throw new NotSupportedException("Plugin instantiation is not supported in this context");
_context = new PluginAssemblyLoadContext(parentContext, entrypointPath); _context = local ? new LocalLoadContext(parentContext, entrypointPath) : new PluginAssemblyLoadContext(parentContext, entrypointPath);
contextBuilder.Add(_context); contextBuilder.Add(_context);
var entrypoint = _context.LoadEntrypoint(); var entrypoint = _context.LoadEntrypoint();
@@ -86,4 +95,70 @@ internal sealed class PluginInstance(PluginMetadata metadata, string entrypointP
Log.Error(ex, "Error opening config"); Log.Error(ex, "Error opening config");
} }
} }
public Task<(DerivedAssemblyLoadContext OldContext, DerivedAssemblyLoadContext NewContext)> ReloadAsync()
{
if (!local)
throw new NotSupportedException("Reload is only supported for local plugins");
if (_disposeTcs != null)
return _disposeTcs.Task;
var tcs = new TaskCompletionSource<(DerivedAssemblyLoadContext OldContext, DerivedAssemblyLoadContext NewContext)>();
_disposeTcs = tcs;
MySandboxGame.Static.Invoke(ReloadInternal, "PluginInstance.Reload");
return tcs.Task;
}
private void ReloadInternal()
{
if (_disposeTcs is null)
throw new InvalidOperationException("Must call Reload first");
Log.Info("Reloading local plugin {Name}", Metadata.Name);
if (_context is null)
throw new InvalidOperationException("Must call Instantiate first");
MyPlugins.m_plugins.Remove(WrappedInstance);
if (_instance is IHandleInputPlugin)
MyPlugins.m_handleInputPlugins.Remove(WrappedInstance);
if (MySession.Static is { } session)
{
foreach (var kvp in session.m_sessionComponents)
{
if (kvp.Key.Assembly == WrappedInstance!.InstanceType.Assembly)
{
session.UnregisterComponent(kvp.Value);
}
}
}
MyGlobalTypeMetadata.Static.m_assemblies.Remove(WrappedInstance!.InstanceType.Assembly);
MyDefinitionManagerBase.m_registered.Remove(WrappedInstance!.InstanceType.Assembly);
MyDefinitionManagerBase.m_registeredAssemblies.Remove(WrappedInstance!.InstanceType.Assembly);
MyXmlSerializerManager.m_registeredAssemblies.Remove(WrappedInstance!.InstanceType.Assembly);
_openConfigAction = null;
WrappedInstance?.Dispose();
WrappedInstance = null;
_instance = null;
_context.Unload();
var oldContext = _context;
var builder = ImmutableArray.CreateBuilder<DerivedAssemblyLoadContext>();
Instantiate(builder);
RegisterLifetime();
WrappedInstance!.Init(MySandboxGame.Static);
Log.Info("Plugin Init: {Metadata}", WrappedInstance.ToString());
MyGlobalTypeMetadata.Static.RegisterAssembly(WrappedInstance!.InstanceType.Assembly);
MySession.Static?.RegisterComponentsFromAssembly(WrappedInstance!.InstanceType.Assembly, true);
_disposeTcs.SetResult((oldContext, builder[0]));
_disposeTcs = null;
Log.Info("Reloaded local plugin {Name}", Metadata.Name);
}
} }

View File

@@ -18,6 +18,7 @@ namespace CringePlugins.Loader;
internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : IPluginsLifetime internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : IPluginsLifetime
{ {
public static ImmutableArray<DerivedAssemblyLoadContext> Contexts { get; private set; } = []; public static ImmutableArray<DerivedAssemblyLoadContext> Contexts { get; private set; } = [];
private static readonly Lock ContextsLock = new();
private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@@ -70,6 +71,23 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) :
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)
{
try
{
var (oldContext, newContext) = await instance.ReloadAsync();
lock(ContextsLock)
{
Contexts = Contexts.Remove(oldContext).Add(newContext);
}
}
catch (Exception e)
{
Log.Error(e, "Failed to reload plugin {Plugin}", instance.Metadata);
}
}
public void RegisterLifetime() public void RegisterLifetime()
{ {
var contextBuilder = Contexts.ToBuilder(); var contextBuilder = Contexts.ToBuilder();
@@ -158,17 +176,17 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) :
var path = files[0].FullName[..^".deps.json".Length] + ".dll"; var path = files[0].FullName[..^".deps.json".Length] + ".dll";
LoadComponent(plugins, path); LoadComponent(plugins, path, null, true);
} }
_plugins = plugins.ToImmutable(); _plugins = plugins.ToImmutable();
} }
private static void LoadComponent(ImmutableArray<PluginInstance>.Builder plugins, string path, PluginMetadata? metadata = null) private static void LoadComponent(ImmutableArray<PluginInstance>.Builder plugins, string path, PluginMetadata? metadata = null, bool local = false)
{ {
try try
{ {
plugins.Add(metadata is null ? new PluginInstance(path) : new(metadata, path)); plugins.Add(metadata is null ? new PluginInstance(path, local) : new(metadata, path, local));
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -14,7 +14,8 @@ public sealed class RenderHandler : IRootRenderComponent
public static RenderHandler Current => _current ?? throw new InvalidOperationException("Render is not yet initialized"); public static RenderHandler Current => _current ?? throw new InvalidOperationException("Render is not yet initialized");
public static IGuiHandler GuiHandler => _guiHandler ?? throw new InvalidOperationException("Render is not yet initialized"); public static IGuiHandler GuiHandler => _guiHandler ?? throw new InvalidOperationException("Render is not yet initialized");
private readonly ConcurrentBag<ComponentRegistration> _components = []; private readonly List<ComponentRegistration> _components = [];
private readonly Lock _componentsLock = new();
internal RenderHandler(IGuiHandler guiHandler) internal RenderHandler(IGuiHandler guiHandler)
{ {
@@ -24,7 +25,24 @@ public sealed class RenderHandler : IRootRenderComponent
public void RegisterComponent<TComponent>(TComponent instance) where TComponent : IRenderComponent public void RegisterComponent<TComponent>(TComponent instance) where TComponent : IRenderComponent
{ {
_components.Add(new ComponentRegistration(typeof(TComponent), instance)); lock (_componentsLock)
_components.Add(new ComponentRegistration(typeof(TComponent), instance));
}
public void UnregisterComponent<TComponent>(TComponent instance) where TComponent : IRenderComponent
{
lock (_componentsLock)
{
for (var i = 0; i < _components.Count; i++)
{
var (instanceType, renderComponent) = _components[i];
if (renderComponent.Equals(instance) && instanceType == typeof(TComponent))
{
_components.RemoveAtFast(i);
return;
}
}
}
} }
void IRenderComponent.OnFrame() void IRenderComponent.OnFrame()
@@ -33,15 +51,18 @@ public sealed class RenderHandler : IRootRenderComponent
ImGui.ShowDemoWindow(); ImGui.ShowDemoWindow();
#endif #endif
foreach (var (instanceType, renderComponent) in _components) lock (_componentsLock)
{ {
try foreach (var (instanceType, renderComponent) in _components)
{ {
renderComponent.OnFrame(); try
} {
catch (Exception e) renderComponent.OnFrame();
{ }
Log.Error(e, "Component {TypeName} failed to render a new frame", instanceType); catch (Exception e)
{
Log.Error(e, "Component {TypeName} failed to render a new frame", instanceType);
}
} }
} }
} }

View File

@@ -127,11 +127,14 @@ internal class PluginListComponent : IRenderComponent
TableNextColumn(); TableNextColumn();
BeginDisabled(!plugin.HasConfig); BeginDisabled(!plugin.HasConfig);
if (plugin.WrappedInstance?.LastException is not null) if (plugin.WrappedInstance?.LastException is not null)
{
PushStyleColor(plugin.HasConfig ? ImGuiCol.Text : ImGuiCol.TextDisabled, PushStyleColor(plugin.HasConfig ? ImGuiCol.Text : ImGuiCol.TextDisabled,
plugin.HasConfig ? Color.Red.ToFloat4() : Color.DarkRed.ToFloat4()); plugin.IsReloading ? Color.Yellow.ToFloat4() : plugin.HasConfig ? Color.Red.ToFloat4() : Color.DarkRed.ToFloat4());
if (Selectable(plugin.Metadata.Name, false, ImGuiSelectableFlags.SpanAllColumns)) }
if (Selectable(plugin.Metadata.Name, false, ImGuiSelectableFlags.SpanAllColumns) && !plugin.IsReloading)
plugin.OpenConfig(); plugin.OpenConfig();
if (plugin.WrappedInstance?.LastException is not null) if (plugin.WrappedInstance?.LastException is not null && !plugin.IsReloading)
{ {
PopStyleColor(); PopStyleColor();
if (IsItemHovered(ImGuiHoveredFlags.ForTooltip)) if (IsItemHovered(ImGuiHoveredFlags.ForTooltip))
@@ -139,6 +142,18 @@ internal class PluginListComponent : IRenderComponent
$"{plugin.WrappedInstance.LastException.GetType()}: {plugin.WrappedInstance.LastException.Message}"); $"{plugin.WrappedInstance.LastException.GetType()}: {plugin.WrappedInstance.LastException.Message}");
} }
EndDisabled(); EndDisabled();
if (!plugin.IsReloading && BeginPopupContextItem($"##{plugin.Metadata.Name}ContextMenu"))
{
BeginDisabled(!plugin.IsLocal);
if (Button("Reload"))
{
PluginsLifetime.ReloadPlugin(plugin).ConfigureAwait(false);
}
EndDisabled();
EndPopup();
}
TableNextColumn(); TableNextColumn();
Text(plugin.Metadata.Version.ToString()); Text(plugin.Metadata.Version.ToString());
TableNextColumn(); TableNextColumn();
@@ -182,11 +197,29 @@ internal class PluginListComponent : IRenderComponent
TableNextColumn(); TableNextColumn();
if (Selectable(source.Name, index == _selectedSource?.index, ImGuiSelectableFlags.SpanAllColumns)) if (Selectable($"{source.Name}##{index}", index == _selectedSource?.index, ImGuiSelectableFlags.SpanAllColumns))
{ {
_selectedSource = (source, index); _selectedSource = (source, index);
} }
if (IsItemActive() && !IsItemHovered())
{
var nextIndex = index + (GetMouseDragDelta(0).Y < 0 ? -1 : 1);
if (nextIndex >= 0 && nextIndex < _packagesConfig.Value.Sources.Length)
{
var builder = _packagesConfig.Value.Sources.ToBuilder();
builder[index] = builder[nextIndex];
builder[nextIndex] = source;
_packagesConfig.Value = _packagesConfig.Value with
{
Sources = builder.ToImmutable()
};
_selectedSource = (source, nextIndex);
}
}
TableNextColumn(); TableNextColumn();
Text(source.Url); Text(source.Url);