diff --git a/CringeLauncher/Patches/ModAssemblyLoadContextPatches.cs b/CringeLauncher/Patches/ModAssemblyLoadContextPatches.cs deleted file mode 100644 index 27e9e0b..0000000 --- a/CringeLauncher/Patches/ModAssemblyLoadContextPatches.cs +++ /dev/null @@ -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 AssemblyNames = []; - - [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[])]); - - 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 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 \ No newline at end of file diff --git a/CringeLauncher/SyntaxRewriters/MissingUsingRewriter.cs b/CringeLauncher/SyntaxRewriters/MissingUsingRewriter.cs index bf37b47..281bf21 100644 --- a/CringeLauncher/SyntaxRewriters/MissingUsingRewriter.cs +++ b/CringeLauncher/SyntaxRewriters/MissingUsingRewriter.cs @@ -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); diff --git a/CringePlugins/Compatability/PluginLoaderConfig.cs b/CringePlugins/Compatability/PluginLoaderConfig.cs index d3d5925..5a7c19d 100644 --- a/CringePlugins/Compatability/PluginLoaderConfig.cs +++ b/CringePlugins/Compatability/PluginLoaderConfig.cs @@ -43,7 +43,7 @@ public class PluginLoaderConfig pluginsBuilder.Add(package); } - var profiles = new Dictionary>(); + var profiles = ImmutableArray.CreateBuilder(); foreach (var profile in Profiles) { var builder = ImmutableArray.CreateBuilder(); @@ -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() }; } diff --git a/CringePlugins/Config/ConfigHandler.cs b/CringePlugins/Config/ConfigHandler.cs index 415186a..b784670 100644 --- a/CringePlugins/Config/ConfigHandler.cs +++ b/CringePlugins/Config/ConfigHandler.cs @@ -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 RegisterConfig(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(_serializerOptions)!; + + T instance; + try + { + instance = jsonNode.Deserialize(_serializerOptions)!; + } + catch (JsonException e) + { + Log.Warn(e, "Failed to load config {Name}", name); + + instance = defaultInstance ?? Activator.CreateInstance(); + } ConfigReloaded?.Invoke(this, new ConfigValue(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(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 : IDisposable { _instance.ConfigReloaded -= InstanceOnConfigReloaded; } - + public static implicit operator T(ConfigReference reference) => reference.Value; } \ No newline at end of file diff --git a/CringePlugins/Config/PackagesConfig.cs b/CringePlugins/Config/PackagesConfig.cs index 718744e..17092b8 100644 --- a/CringePlugins/Config/PackagesConfig.cs +++ b/CringePlugins/Config/PackagesConfig.cs @@ -4,7 +4,7 @@ using NuGet; namespace CringePlugins.Config; -public record PackagesConfig(ImmutableArray Sources, ImmutableArray Packages, Dictionary> Profiles) +public record PackagesConfig(ImmutableArray Sources, ImmutableArray Packages, ImmutableArray 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 Sources, ImmutableArr [ new PackageReference("Plugin.ClientModLoader", new(new(0,0,0))) ], - []); //todo: default profile with recommended plugins? -} \ No newline at end of file + []); +} +public record Profile(string Id, ImmutableArray Plugins); \ No newline at end of file diff --git a/CringePlugins/Ui/PluginListComponent.cs b/CringePlugins/Ui/PluginListComponent.cs index 010c36d..2e29832 100644 --- a/CringePlugins/Ui/PluginListComponent.cs +++ b/CringePlugins/Ui/PluginListComponent.cs @@ -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 _profiles; + + private bool _changed; private bool _open = true; private readonly ConfigReference _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 } } \ No newline at end of file