11
0

Add support for plugin profiles
All checks were successful
Build / Compute Version (push) Successful in 6s
Build / Build Nuget package (NuGet) (push) Successful in 3m59s
Build / Build Nuget package (CringeBootstrap.Abstractions) (push) Successful in 4m5s
Build / Build Nuget package (SharedCringe) (push) Successful in 4m2s
Build / Build Nuget package (CringePlugins) (push) Successful in 4m20s
Build / Build Launcher (push) Successful in 5m11s

Some minor cleanup
This commit is contained in:
pas2704 2025-06-04 16:47:24 -04:00
parent 05556c7841
commit bc88f0c28a
6 changed files with 228 additions and 130 deletions

View File

@ -1,96 +0,0 @@
#if false
using System.Diagnostics;
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.Collections;
using VRage.Scripting;
namespace CringeLauncher.Patches;
[HarmonyPatch]
public static class ModAssemblyLoadContextPatches //todo: use ModScriptCompilerPatch
{
private static ModAssemblyLoadContext? _currentSessionContext;
private static readonly MyConcurrentHashSet<string> AssemblyNames = [];
[HarmonyPatch(typeof(MyScriptCompiler), nameof(MyScriptCompiler.Compile), MethodType.Async)]
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> CompilerTranspiler(IEnumerable<CodeInstruction> 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[])]);
return 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));
}))
.Start()
.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));
}))
.Instructions();
}
[HarmonyPatch(typeof(MyScriptManager), "Compile")]
[HarmonyPrefix]
private static bool CompilePrefix(string assemblyName)
{
if (!AssemblyNames.Add(assemblyName))
{
Debug.WriteLine($"Duplicate assembly: {assemblyName}");
return false;
}
return true;
}
[HarmonyPatch(typeof(MySession), nameof(MySession.Unload))]
[HarmonyPostfix]
private static void UnloadPostfix()
{
AssemblyNames.Clear();
if (_currentSessionContext is null) return;
_currentSessionContext.Unload();
_currentSessionContext = null;
}
[HarmonyPatch]
private static class LoadPrefixes
{
[HarmonyTargetMethods]
private static IEnumerable<MethodInfo> TargetMethods()
{
yield return AccessTools.Method(typeof(MySession), nameof(MySession.Load));
yield return AccessTools.Method(typeof(MySession), "LoadMultiplayer");
}
[HarmonyPrefix]
private static void Prefix()
{
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);
}
}
}
#endif

View File

@ -10,7 +10,7 @@ internal sealed class MissingUsingRewriter : ProtoTagRewriter //use existing rew
private readonly SemanticModel _semanticModel;
private MissingUsingRewriter(CSharpCompilation compilation, SyntaxTree tree) : base(compilation, tree) => _semanticModel = compilation.GetSemanticModel(tree);
public static SyntaxTree Rewrite(CSharpCompilation compilation, SyntaxTree tree)
public static new SyntaxTree Rewrite(CSharpCompilation compilation, SyntaxTree tree)
{
SyntaxNode syntaxNode = new MissingUsingRewriter(compilation, tree).Visit(tree.GetRoot());
return tree.WithRootAndOptions(syntaxNode, tree.Options);

View File

@ -43,7 +43,7 @@ public class PluginLoaderConfig
pluginsBuilder.Add(package);
}
var profiles = new Dictionary<string, ImmutableArray<PackageReference>>();
var profiles = ImmutableArray.CreateBuilder<Profile>();
foreach (var profile in Profiles)
{
var builder = ImmutableArray.CreateBuilder<PackageReference>();
@ -62,13 +62,13 @@ public class PluginLoaderConfig
builder.Add(package);
}
profiles[profile.Name] = builder.ToImmutable();
profiles.Add(new(profile.Name, builder.ToImmutable()));
}
return old with
{
Packages = pluginsBuilder.ToImmutable(),
Profiles = profiles,
Profiles = profiles.ToImmutable(),
Sources = sources.ToImmutable()
};
}

View File

@ -10,7 +10,7 @@ namespace CringePlugins.Config;
public sealed class ConfigHandler
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private readonly DirectoryInfo _configDirectory;
private readonly JsonSerializerOptions _serializerOptions = new(NuGetClient.SerializerOptions)
{
@ -35,7 +35,7 @@ public sealed class ConfigHandler
public ConfigReference<T> RegisterConfig<T>(string name, T? defaultInstance = null) where T : class
{
var spec = IConfigurationSpecProvider.FromType(typeof(T));
var path = Path.Join(_configDirectory.FullName, $"{name}.json");
var backupPath = path + $".bak.{DateTimeOffset.Now.ToUnixTimeSeconds()}";
@ -63,10 +63,20 @@ public sealed class ConfigHandler
RegisterChange(name, defaultInstance);
return reference;
}
var instance = jsonNode.Deserialize<T>(_serializerOptions)!;
T instance;
try
{
instance = jsonNode.Deserialize<T>(_serializerOptions)!;
}
catch (JsonException e)
{
Log.Warn(e, "Failed to load config {Name}", name);
instance = defaultInstance ?? Activator.CreateInstance<T>();
}
ConfigReloaded?.Invoke(this, new ConfigValue<T>(name, instance));
return reference;
}
@ -78,16 +88,16 @@ public sealed class ConfigHandler
if (spec != null && !TryValidate(name, spec, jsonNode))
throw new JsonException($"Supplied config value for {name} is invalid");
var path = Path.Join(_configDirectory.FullName, $"{name}.json");
using var stream = File.Create(path);
using var writer = new Utf8JsonWriter(stream, new()
{
Indented = true
});
jsonNode.WriteTo(writer, _serializerOptions);
ConfigReloaded?.Invoke(this, new ConfigValue<T>(name, newValue));
}
@ -97,7 +107,7 @@ public sealed class ConfigHandler
if (results.IsValid)
return true;
Log.Error("Config {Name} is invalid:", name);
foreach (var detail in results.Details)
{
@ -107,7 +117,7 @@ public sealed class ConfigHandler
Log.Error("\t- {Error}", error);
}
}
return false;
}
@ -145,6 +155,6 @@ public sealed class ConfigReference<T> : IDisposable
{
_instance.ConfigReloaded -= InstanceOnConfigReloaded;
}
public static implicit operator T(ConfigReference<T> reference) => reference.Value;
}

View File

@ -4,7 +4,7 @@ using NuGet;
namespace CringePlugins.Config;
public record PackagesConfig(ImmutableArray<PackageSource> Sources, ImmutableArray<PackageReference> Packages, Dictionary<string, ImmutableArray<PackageReference>> Profiles)
public record PackagesConfig(ImmutableArray<PackageSource> Sources, ImmutableArray<PackageReference> Packages, ImmutableArray<Profile> Profiles)
{
public static PackagesConfig Default { get; } = new([
new("zznty", @"^SpaceEngineersDedicated\.ReferenceAssemblies$|^ImGui\.NET\.DirectX$|^NuGet$|^Cringe.+$|^SharedCringe$|^Plugin.+$", "https://ng.zznty.ru/v3/index.json"),
@ -13,5 +13,6 @@ public record PackagesConfig(ImmutableArray<PackageSource> Sources, ImmutableArr
[
new PackageReference("Plugin.ClientModLoader", new(new(0,0,0)))
],
[]); //todo: default profile with recommended plugins?
}
[]);
}
public record Profile(string Id, ImmutableArray<PackageReference> Plugins);

View File

@ -1,8 +1,4 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text.Json;
using System.Xml.Serialization;
using CringePlugins.Abstractions;
using CringePlugins.Abstractions;
using CringePlugins.Compatability;
using CringePlugins.Config;
using CringePlugins.Loader;
@ -18,6 +14,11 @@ using NuGet.Versioning;
using Sandbox.Game.Gui;
using Sandbox.Graphics.GUI;
using SpaceEngineers.Game.GUI;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Numerics;
using System.Text.Json;
using System.Xml.Serialization;
using static ImGuiNET.ImGui;
namespace CringePlugins.Ui;
@ -32,6 +33,12 @@ internal class PluginListComponent : IRenderComponent
private string _searchQuery = "";
private Task? _searchTask;
private string _profileSearch = "";
private string _newProfileName = "";
private int _selectedProfile = -1;
private ImmutableArray<Profile> _profiles;
private bool _changed;
private bool _open = true;
private readonly ConfigReference<PackagesConfig> _packagesConfig;
@ -53,6 +60,7 @@ internal class PluginListComponent : IRenderComponent
_plugins = plugins;
_packages = packagesConfig.Value.Packages.ToImmutableDictionary(b => b.Id, b => b.Range,
StringComparer.OrdinalIgnoreCase);
_profiles = packagesConfig.Value.Profiles;
MyScreenManager.ScreenAdded += ScreenChanged;
MyScreenManager.ScreenRemoved += ScreenChanged;
@ -92,9 +100,9 @@ internal class PluginListComponent : IRenderComponent
{
if (BeginTable("InstalledTable", 3, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.Sortable))
{
TableSetupColumn("Id", ImGuiTableColumnFlags.None, .5f, (uint)Columns.Id);
TableSetupColumn("Version", ImGuiTableColumnFlags.None, .25f, (uint)Columns.Version);
TableSetupColumn("Source", ImGuiTableColumnFlags.None, .25f, (uint)Columns.Source);
TableSetupColumn("Id", ImGuiTableColumnFlags.WidthStretch, .5f, (uint)Columns.Id);
TableSetupColumn("Version", ImGuiTableColumnFlags.WidthStretch, .25f, (uint)Columns.Version);
TableSetupColumn("Source", ImGuiTableColumnFlags.WidthStretch, .25f, (uint)Columns.Source);
TableHeadersRow();
var sortSpecs = TableGetSortSpecs();
@ -143,6 +151,12 @@ internal class PluginListComponent : IRenderComponent
EndTabItem();
}
if (BeginTabItem("Profiles"))
{
ProfilesTab();
EndTabItem();
}
if (BeginTabItem("Sources Configuration"))
{
BeginChild("Sources List", new(400, 0), ImGuiChildFlags.Border | ImGuiChildFlags.ResizeX);
@ -150,8 +164,8 @@ internal class PluginListComponent : IRenderComponent
if (BeginTable("Sources Table", 2,
ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp))
{
TableSetupColumn("Name", ImGuiTableColumnFlags.None, .2f);
TableSetupColumn("Url", ImGuiTableColumnFlags.None, .8f);
TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, .2f);
TableSetupColumn("Url", ImGuiTableColumnFlags.WidthStretch, .8f);
TableHeadersRow();
for (var index = 0; index < _packagesConfig.Value.Sources.Length; index++)
@ -321,6 +335,173 @@ internal class PluginListComponent : IRenderComponent
End();
}
private unsafe void ProfilesTab()
{
InputText("##searchbox", ref _profileSearch, 256);
SameLine();
if (Button("Create New Profile"))
OpenPopup("New Profile");
if (IsItemHovered(ImGuiHoveredFlags.ForTooltip))
{
SetTooltip("Create a new profile from enabled plugins");
}
if (BeginPopupModal("New Profile", ImGuiWindowFlags.AlwaysAutoResize))
{
InputText("Name", ref _newProfileName, 50);
Separator();
if (Button("Ok##newProfileOk", new Vector2(120, 0)))
{
var len = _profiles.Length;
_profiles = _profiles.Add(new(_newProfileName, [.. _packages.Select(x => new PackageReference(x.Key, x.Value))]));
_selectedProfile = len;
_packagesConfig.Value = _packagesConfig.Value with
{
Profiles = _profiles
};
CloseCurrentPopup();
}
SetItemDefaultFocus();
SameLine();
if (Button("Cancel##newProfileCancel", new Vector2(120, 0)))
{
CloseCurrentPopup();
}
EndPopup();
}
Spacing();
if (_profiles.IsEmpty)
{
TextDisabled("No Profiles");
return;
}
BeginChild("Profile List", new(400, 0), ImGuiChildFlags.Border | ImGuiChildFlags.ResizeX);
if (BeginTable("ProfilesTable", 2,
ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp))
{
TableSetupColumn("Id##ProfilesTable", ImGuiTableColumnFlags.WidthStretch, .5f, (uint)Columns.Id);
TableSetupColumn("Plugins", ImGuiTableColumnFlags.WidthStretch, .25f, (uint)Columns.Count);
TableHeadersRow();
for (var i = 0; i < _profiles.Length; i++)
{
var(id, plugins) = _profiles[i];
if (!id.Contains(_profileSearch, StringComparison.OrdinalIgnoreCase))
continue;
TableNextRow();
TableNextColumn();
var selected = _selectedProfile == i;
if (Selectable($"{id}##profiles{i}", ref selected, ImGuiSelectableFlags.SpanAllColumns))
{
_selectedProfile = selected ? i : -1;
}
TableNextColumn();
Text(plugins.Length.ToString());
}
EndTable();
}
EndChild();
SameLine();
BeginGroup();
BeginChild("Profile View", new(0, -GetFrameHeightWithSpacing())); // Leave room for 1 line below us
if (_selectedProfile >= 0)
{
var (id, plugins) = _profiles[_selectedProfile];
Text(id);
Separator();
if (BeginTable("ProfilePluginsTable", 2, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp))
{
TableSetupColumn("Id##pluginProfilesId", ImGuiTableColumnFlags.WidthStretch, .5f, (uint)Columns.Id);
TableSetupColumn("Version##pluginProfilesVersion", ImGuiTableColumnFlags.WidthStretch, .25f, (uint)Columns.Version);
foreach (var plugin in plugins)
{
TableNextRow();
TableNextColumn();
Text(plugin.Id);
TableNextColumn();
Text(plugin.Range.ToShortString());
}
EndTable();
}
}
EndChild();
if (_selectedProfile >= 0)
{
if (Button("Activate"))
{
_packages = _profiles[_selectedProfile].Plugins.ToImmutableDictionary(b => b.Id, b => b.Range);
Save();
}
SameLine();
if (Button("Delete"))
OpenPopup("Delete?##ProfileDeletePopup");
if (BeginPopupModal("Delete?##ProfileDeletePopup", ImGuiWindowFlags.AlwaysAutoResize))
{
Text("Are you sure you want to delete this profile?");
Separator();
if (Button("Yes", new Vector2(120, 0)))
{
_profiles = _profiles.RemoveAt(_selectedProfile);
_packagesConfig.Value = _packagesConfig.Value with
{
Profiles = _profiles
};
_selectedProfile = -1;
CloseCurrentPopup();
}
SetItemDefaultFocus();
SameLine();
if (Button("No", new Vector2(120, 0)))
{
CloseCurrentPopup();
}
EndPopup();
}
}
EndGroup();
}
// TODO sources editor
// TODO combobox with active sources (to limit search results to specific list of sources)
private unsafe void AvailablePluginsTab()
@ -357,6 +538,7 @@ internal class PluginListComponent : IRenderComponent
: (_selectedSources ?? _packagesConfig.Value.Sources.ToImmutableHashSet()).Remove(source);
_searchTask = RefreshAsync();
EndCombo();
return;
}
}
@ -398,9 +580,9 @@ internal class PluginListComponent : IRenderComponent
if (BeginTable("AvailableTable", 3,
ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.Sortable))
{
TableSetupColumn("Id", ImGuiTableColumnFlags.None, .5f, (uint)Columns.Id);
TableSetupColumn("Version", ImGuiTableColumnFlags.None, .25f, (uint)Columns.Version);
TableSetupColumn("Installed", ImGuiTableColumnFlags.None, .25f, (uint)Columns.Installed);
TableSetupColumn("Id", ImGuiTableColumnFlags.WidthStretch, .5f, (uint)Columns.Id);
TableSetupColumn("Version", ImGuiTableColumnFlags.WidthStretch, .25f, (uint)Columns.Version);
TableSetupColumn("Installed", ImGuiTableColumnFlags.WidthStretch, .25f, (uint)Columns.Installed);
TableHeadersRow();
var sortSpecs = TableGetSortSpecs();
@ -569,9 +751,9 @@ internal class PluginListComponent : IRenderComponent
_searchResults = builder.ToImmutable();
}
private void Save(bool keepPackages = true)
private void Save(bool keepChanges = true)
{
_packagesConfig.Value = keepPackages ? _packagesConfig.Value with
_packagesConfig.Value = keepChanges ? _packagesConfig.Value with
{
Packages = [.. _packages.Select(b => new PackageReference(b.Key, b.Value))]
} : _packagesConfig;
@ -649,6 +831,7 @@ internal class PluginListComponent : IRenderComponent
Id,
Version,
Source,
Installed
Installed,
Count
}
}