11
0

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:
pas2704 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.Render11:VRageRender.MyCommon" />
<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>

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

View File

@ -3,17 +3,26 @@ using System.Runtime.Loader;
using CringeBootstrap.Abstractions;
using CringePlugins.Utils;
using NLog;
using Sandbox;
using Sandbox.Game.World;
using SharedCringe.Loader;
using VRage;
using VRage.Game;
using VRage.Game.ObjectBuilder;
using VRage.Plugins;
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 IsReloading => _disposeTcs?.Task.IsCompleted == false;
public bool IsLocal => local;
private PluginAssemblyLoadContext? _context;
private IPlugin? _instance;
private TaskCompletionSource<(DerivedAssemblyLoadContext OldContext, DerivedAssemblyLoadContext NewContext)>? _disposeTcs;
private Action? _openConfigAction;
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();
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)
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);
var entrypoint = _context.LoadEntrypoint();
@ -86,4 +95,70 @@ internal sealed class PluginInstance(PluginMetadata metadata, string entrypointP
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
{
public static ImmutableArray<DerivedAssemblyLoadContext> Contexts { get; private set; } = [];
private static readonly Lock ContextsLock = new();
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));
}
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()
{
var contextBuilder = Contexts.ToBuilder();
@ -158,17 +176,17 @@ internal class PluginsLifetime(ConfigHandler configHandler, HttpClient client) :
var path = files[0].FullName[..^".deps.json".Length] + ".dll";
LoadComponent(plugins, path);
LoadComponent(plugins, path, null, true);
}
_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
{
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)
{

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 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)
{
@ -24,7 +25,24 @@ public sealed class RenderHandler : IRootRenderComponent
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()
@ -33,15 +51,18 @@ public sealed class RenderHandler : IRootRenderComponent
ImGui.ShowDemoWindow();
#endif
foreach (var (instanceType, renderComponent) in _components)
lock (_componentsLock)
{
try
foreach (var (instanceType, renderComponent) in _components)
{
renderComponent.OnFrame();
}
catch (Exception e)
{
Log.Error(e, "Component {TypeName} failed to render a new frame", instanceType);
try
{
renderComponent.OnFrame();
}
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();
BeginDisabled(!plugin.HasConfig);
if (plugin.WrappedInstance?.LastException is not null)
{
PushStyleColor(plugin.HasConfig ? ImGuiCol.Text : ImGuiCol.TextDisabled,
plugin.HasConfig ? Color.Red.ToFloat4() : Color.DarkRed.ToFloat4());
if (Selectable(plugin.Metadata.Name, false, ImGuiSelectableFlags.SpanAllColumns))
plugin.IsReloading ? Color.Yellow.ToFloat4() : plugin.HasConfig ? Color.Red.ToFloat4() : Color.DarkRed.ToFloat4());
}
if (Selectable(plugin.Metadata.Name, false, ImGuiSelectableFlags.SpanAllColumns) && !plugin.IsReloading)
plugin.OpenConfig();
if (plugin.WrappedInstance?.LastException is not null)
if (plugin.WrappedInstance?.LastException is not null && !plugin.IsReloading)
{
PopStyleColor();
if (IsItemHovered(ImGuiHoveredFlags.ForTooltip))
@ -139,6 +142,18 @@ internal class PluginListComponent : IRenderComponent
$"{plugin.WrappedInstance.LastException.GetType()}: {plugin.WrappedInstance.LastException.Message}");
}
EndDisabled();
if (!plugin.IsReloading && BeginPopupContextItem($"##{plugin.Metadata.Name}ContextMenu"))
{
BeginDisabled(!plugin.IsLocal);
if (Button("Reload"))
{
PluginsLifetime.ReloadPlugin(plugin).ConfigureAwait(false);
}
EndDisabled();
EndPopup();
}
TableNextColumn();
Text(plugin.Metadata.Version.ToString());
TableNextColumn();
@ -182,11 +197,29 @@ internal class PluginListComponent : IRenderComponent
TableNextColumn();
if (Selectable(source.Name, index == _selectedSource?.index, ImGuiSelectableFlags.SpanAllColumns))
if (Selectable($"{source.Name}##{index}", index == _selectedSource?.index, ImGuiSelectableFlags.SpanAllColumns))
{
_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();
Text(source.Url);