diff --git a/CringeLauncher/CringeLauncher.csproj b/CringeLauncher/CringeLauncher.csproj
index 9c2ccb7..0a960ee 100644
--- a/CringeLauncher/CringeLauncher.csproj
+++ b/CringeLauncher/CringeLauncher.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/CringeLauncher/ImGuiHandler.cs b/CringeLauncher/ImGuiHandler.cs
index c4b0a96..c8efbee 100644
--- a/CringeLauncher/ImGuiHandler.cs
+++ b/CringeLauncher/ImGuiHandler.cs
@@ -14,20 +14,18 @@ namespace CringeLauncher;
internal class ImGuiHandler : IDisposable
{
- private readonly DeviceContext _deviceContext;
+ private DeviceContext? _deviceContext;
private static nint _wndproc;
public static ImGuiHandler? Instance;
public static RenderTargetView? Rtv;
- private readonly IRootRenderComponent _renderHandler;
+ private readonly IRootRenderComponent _renderHandler = new RenderHandler();
- public ImGuiHandler(nint windowHandle, Device1 device, DeviceContext deviceContext)
+ public void Init(nint windowHandle, Device1 device, DeviceContext deviceContext)
{
_deviceContext = deviceContext;
- _renderHandler = new RenderHandler();
-
CreateContext();
var io = GetIO();
@@ -64,7 +62,7 @@ internal class ImGuiHandler : IDisposable
Render();
- _deviceContext.ClearState();
+ _deviceContext!.ClearState();
_deviceContext.OutputMerger.SetRenderTargets(Rtv);
ImGui_ImplDX11_RenderDrawData(GetDrawData());
@@ -87,7 +85,7 @@ internal class ImGuiHandler : IDisposable
public void Dispose()
{
- _deviceContext.Dispose();
+ _deviceContext?.Dispose();
_renderHandler.Dispose();
}
}
\ No newline at end of file
diff --git a/CringeLauncher/Launcher.cs b/CringeLauncher/Launcher.cs
index 5ab9fc1..f79f493 100644
--- a/CringeLauncher/Launcher.cs
+++ b/CringeLauncher/Launcher.cs
@@ -101,8 +101,8 @@ public class Launcher : ICorePlugin
_renderComponent = new();
_renderComponent.Start(new(), InitEarlyWindow, MyVideoSettingsManager.Initialize(), MyPerGameSettings.MaxFrameRate);
_renderComponent.RenderThread.BeforeDraw += MyFpsManager.Update;
- _renderComponent.RenderThread.SizeChanged += RenderThreadOnSizeChanged;
+ // this technically should wait for render thread init, but who cares
splash.ExecuteLoadingStages();
InitUgc();
@@ -118,21 +118,20 @@ public class Launcher : ICorePlugin
_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()
+ private IVRageWindow InitEarlyWindow()
{
+ ImGuiHandler.Instance = new();
+
MyVRage.Platform.Windows.CreateWindow("Cringe Launcher", MyPerGameSettings.GameIcon, null);
MyVRage.Platform.Windows.Window.OnExit += MySandboxGame.ExitThreadSafe;
+
+ _renderComponent!.RenderThread.UpdateSize();
+ MyRenderProxy.RenderThread = _renderComponent.RenderThread;
+
+ MyVRage.Platform.Windows.Window.ShowAndFocus();
return MyVRage.Platform.Windows.Window;
}
@@ -157,7 +156,8 @@ public class Launcher : ICorePlugin
private static void InitThreadPool()
{
- ParallelTasks.Parallel.Scheduler = new ThreadPoolScheduler();
+ // ParallelTasks.Parallel.Scheduler = new ThreadPoolScheduler();
+ MySandboxGame.InitMultithreading();
}
private static void ConfigureSettings()
diff --git a/CringeLauncher/Patches/SwapChainPatch.cs b/CringeLauncher/Patches/SwapChainPatch.cs
index e552dd0..df5238d 100644
--- a/CringeLauncher/Patches/SwapChainPatch.cs
+++ b/CringeLauncher/Patches/SwapChainPatch.cs
@@ -60,7 +60,7 @@ public static class SwapChainPatch
[HarmonyPostfix, HarmonyPatch(typeof(MyRender11), nameof(MyRender11.CreateDeviceInternal))]
private static void CreateDevicePostfix()
{
- ImGuiHandler.Instance ??= new ImGuiHandler(WindowHandle, MyRender11.DeviceInstance, MyRender11.RC.DeviceContext);
+ ImGuiHandler.Instance?.Init(WindowHandle, MyRender11.DeviceInstance, MyRender11.RC.DeviceContext);
}
[HarmonyPrefix, HarmonyPatch(typeof(MyBackbuffer), MethodType.Constructor, typeof(SharpDX.Direct3D11.Resource))]
diff --git a/CringeLauncher/Utils/ThreadPoolScheduler.cs b/CringeLauncher/Utils/ThreadPoolScheduler.cs
index 0a641af..e5e7108 100644
--- a/CringeLauncher/Utils/ThreadPoolScheduler.cs
+++ b/CringeLauncher/Utils/ThreadPoolScheduler.cs
@@ -8,6 +8,7 @@ using CringeTask = ParallelTasks.Task;
namespace CringeLauncher.Utils;
+/*
public class ThreadPoolScheduler : IWorkScheduler
{
public void Schedule(CringeTask item)
@@ -82,4 +83,4 @@ internal class ThreadPoolWorkItemTask(CringeTask task) : IThreadPoolWorkItem
HkBaseSystem.QuitThread();
Debug.WriteLine($"Hk Shutdown for {Thread.CurrentThread.Name}");
}
-}
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/CringePlugins/Config/PackagesConfig.cs b/CringePlugins/Config/PackagesConfig.cs
index 1da804c..87cf5e6 100644
--- a/CringePlugins/Config/PackagesConfig.cs
+++ b/CringePlugins/Config/PackagesConfig.cs
@@ -8,9 +8,7 @@ namespace CringePlugins.Config;
public record PackagesConfig(ImmutableArray Sources, ImmutableArray Packages)
{
public static PackagesConfig Default { get; } = new([
- new("SpaceEngineersDedicated.ReferenceAssemblies", "https://ng.zznty.ru/v3/index.json"),
- new("ImGui.NET.DirectX", "https://ng.zznty.ru/v3/index.json"),
- new("Plugin", "https://ng.zznty.ru/v3/index.json"),
+ new(@"^SpaceEngineersDedicated\.ReferenceAssemblies$|^ImGui\.NET\.DirectX$|^NuGet$|^Cringe.+$|^SharedCringe$|^Plugin.+$", "https://ng.zznty.ru/v3/index.json"),
new(string.Empty, "https://api.nuget.org/v3/index.json")
], []);
}
\ No newline at end of file
diff --git a/CringePlugins/Loader/PluginsLifetime.cs b/CringePlugins/Loader/PluginsLifetime.cs
index 88fed83..743252d 100644
--- a/CringePlugins/Loader/PluginsLifetime.cs
+++ b/CringePlugins/Loader/PluginsLifetime.cs
@@ -2,8 +2,10 @@
using System.Runtime.InteropServices;
using System.Text.Json;
using CringePlugins.Config;
+using CringePlugins.Render;
using CringePlugins.Resolver;
using CringePlugins.Splash;
+using CringePlugins.Ui;
using NLog;
using NuGet;
using NuGet.Deps;
@@ -63,6 +65,8 @@ public class PluginsLifetime : ILoadingStage
progress.Report("Registering plugins");
RegisterLifetime();
+
+ RenderHandler.Current.RegisterComponent(new PluginListComponent(packagesConfig, sourceMapping, configPath));
}
private void RegisterLifetime()
diff --git a/CringePlugins/Render/RenderHandler.cs b/CringePlugins/Render/RenderHandler.cs
index 13f6d7c..9d800ef 100644
--- a/CringePlugins/Render/RenderHandler.cs
+++ b/CringePlugins/Render/RenderHandler.cs
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using CringePlugins.Abstractions;
+using ImGuiNET;
using NLog;
namespace CringePlugins.Render;
@@ -25,6 +26,10 @@ public sealed class RenderHandler : IRootRenderComponent
void IRenderComponent.OnFrame()
{
+#if DEBUG
+ ImGui.ShowDemoWindow();
+#endif
+
foreach (var (instanceType, renderComponent) in _components)
{
try
diff --git a/CringePlugins/Resolver/BuiltInPackages.cs b/CringePlugins/Resolver/BuiltInPackages.cs
index 577a670..0715e69 100644
--- a/CringePlugins/Resolver/BuiltInPackages.cs
+++ b/CringePlugins/Resolver/BuiltInPackages.cs
@@ -53,7 +53,12 @@ public static class BuiltInPackages
se,
imGui,
harmony,
- FromAssembly(runtimeFramework, [se.AsDependency(), imGui.AsDependency(), harmony.AsDependency()]),
+ FromAssembly(runtimeFramework,
+ [se.AsDependency(), imGui.AsDependency(), harmony.AsDependency()]
+#if DEBUG
+ , version: new(0, 1, 4)
+#endif
+ ),
];
}
diff --git a/CringePlugins/Resolver/PackageResolver.cs b/CringePlugins/Resolver/PackageResolver.cs
index ec0e6ac..90ec8d4 100644
--- a/CringePlugins/Resolver/PackageResolver.cs
+++ b/CringePlugins/Resolver/PackageResolver.cs
@@ -21,7 +21,7 @@ public class PackageResolver(NuGetFramework runtimeFramework, ImmutableArray
- page.Items.Where(b => b.CatalogEntry.PackageTypes is ["CringePlugin"]))
+ page.Items!.Where(b => b.CatalogEntry.PackageTypes is ["CringePlugin"]))
.ToImmutableDictionary(b => b.CatalogEntry.Version);
var version = reference.Range.FindBestMatch(items.Values.Select(b => b.CatalogEntry.Version));
@@ -56,8 +56,11 @@ public class PackageResolver(NuGetFramework runtimeFramework, ImmutableArray g.TargetFramework);
if (nearestGroup is null)
diff --git a/CringePlugins/Ui/PluginListComponent.cs b/CringePlugins/Ui/PluginListComponent.cs
new file mode 100644
index 0000000..6cc427f
--- /dev/null
+++ b/CringePlugins/Ui/PluginListComponent.cs
@@ -0,0 +1,310 @@
+using System.Collections.Immutable;
+using System.Text.Json;
+using CringePlugins.Abstractions;
+using CringePlugins.Config;
+using CringePlugins.Resolver;
+using ImGuiNET;
+using NLog;
+using NuGet;
+using NuGet.Models;
+using NuGet.Versioning;
+using Sandbox.Graphics.GUI;
+using SpaceEngineers.Game.GUI;
+using static ImGuiNET.ImGui;
+
+namespace CringePlugins.Ui;
+
+public class PluginListComponent : IRenderComponent
+{
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+ private ImmutableDictionary _packages;
+ private ImmutableDictionary? _searchResults;
+ private string _searchQuery = "";
+ private Task? _searchTask;
+
+ private bool _changed;
+ private bool _open = true;
+ private readonly PackagesConfig _packagesConfig;
+ private readonly PackageSourceMapping _sources;
+ private readonly string _configPath;
+ private (SearchResultEntry entry, NuGetClient client)? _selected;
+
+ public PluginListComponent(PackagesConfig packagesConfig, PackageSourceMapping sources, string configPath)
+ {
+ _packagesConfig = packagesConfig;
+ _sources = sources;
+ _configPath = configPath;
+ _packages = packagesConfig.Packages.ToImmutableDictionary(b => b.Id, b => b.Range, StringComparer.OrdinalIgnoreCase);
+
+ MyGuiSandbox.GuiControlCreated += GuiControlCreated;
+ }
+
+ private void GuiControlCreated(object obj)
+ {
+ if (obj is MyGuiScreenMainMenu)
+ _open = true;
+ }
+
+ public void OnFrame()
+ {
+ if (!_open) return;
+
+ SetNextWindowSize(new(700, 500), ImGuiCond.FirstUseEver);
+
+ if (!Begin("Plugin List", ref _open))
+ {
+ End();
+ return;
+ }
+
+ if (_changed)
+ TextDisabled("Changes would be applied on the next restart");
+
+ if (BeginTabBar("Main"))
+ {
+ // TODO support for opening plugin loader plugin config (reflection call to a specific method)
+ if (BeginTabItem("Installed Plugins"))
+ {
+ if (BeginTable("InstalledTable", 2, ImGuiTableFlags.ScrollY))
+ {
+ TableSetupColumn("Id");
+ TableSetupColumn("Version");
+ TableHeadersRow();
+
+ foreach (var (id, versionRange) in _packages)
+ {
+ TableNextRow();
+
+ TableNextColumn();
+ Text(id);
+ TableNextColumn();
+ Text(versionRange.MinVersion?.ToString() ?? versionRange.ToString());
+ }
+
+ EndTable();
+ }
+
+ EndTabItem();
+ }
+
+ if (BeginTabItem("Available Plugins"))
+ {
+ AvailablePluginsTab();
+
+ EndTabItem();
+ }
+
+ EndTabBar();
+ }
+
+ End();
+ }
+
+ // TODO sources editor
+ // TODO combobox with active sources (to limit search results to specific list of sources)
+ private unsafe void AvailablePluginsTab()
+ {
+ if (_searchResults is null && _searchTask is null)
+ {
+ _searchTask = RefreshAsync();
+ }
+
+ switch (_searchTask)
+ {
+ case { IsCompleted: false }:
+ TextDisabled("Loading...");
+ return;
+ case { IsCompletedSuccessfully: false }:
+ {
+ TextDisabled("Failed to load plugins list");
+ if (_searchTask.Exception is null) return;
+
+ foreach (var exception in _searchTask.Exception.InnerExceptions)
+ {
+ TextWrapped($"{exception.GetType()}: {exception.Message}");
+ }
+
+ return;
+ }
+ }
+
+ var searchResults = _searchResults ?? ImmutableDictionary.Empty;
+
+ InputText("", ref _searchQuery, 256);
+
+ SameLine();
+
+ if (Button("Search"))
+ {
+ _searchTask = RefreshAsync();
+ return;
+ }
+
+ if (searchResults.IsEmpty || searchResults.Values.All(b => b.Entries.IsEmpty))
+ {
+ TextDisabled("Nothing found");
+ return;
+ }
+
+ BeginChild("List", new(400, 0), ImGuiChildFlags.Border | ImGuiChildFlags.ResizeX);
+ {
+ if (BeginTable("AvailableTable", 3, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp))
+ {
+ TableSetupColumn("Id", ImGuiTableColumnFlags.None, .5f);
+ TableSetupColumn("Version", ImGuiTableColumnFlags.None, .3f);
+ TableSetupColumn("Installed", ImGuiTableColumnFlags.None, .2f);
+ TableHeadersRow();
+
+ foreach (var (client, result) in searchResults)
+ {
+ foreach (var package in result.Entries.Take(100))
+ {
+ TableNextRow();
+
+ TableNextColumn();
+
+ var selected = _selected?.entry.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase) == true;
+
+ if (Selectable(package.Title ?? package.Id, ref selected, ImGuiSelectableFlags.SpanAllColumns))
+ {
+ _selected = selected ? (package, client) : null;
+ }
+
+ if (!string.IsNullOrEmpty(package.Summary) && IsItemHovered(ImGuiHoveredFlags.ForTooltip))
+ {
+ SetTooltip(package.Summary);
+ }
+
+ TableNextColumn();
+ Text(package.Version.ToString());
+
+ TableNextColumn();
+
+ var installed = _packages.ContainsKey(package.Id);
+ BeginDisabled();
+ if (Checkbox("", ref installed))
+ {
+ _packages = installed
+ ? _packages.Add(package.Id, new(package.Version))
+ : _packages.Remove(package.Id);
+
+ Save();
+ }
+ EndDisabled();
+ }
+ }
+
+ EndTable();
+ }
+
+ EndChild();
+ }
+
+ SameLine();
+
+ BeginGroup();
+
+ BeginChild("Package View", new(0, -GetFrameHeightWithSpacing())); // Leave room for 1 line below us
+
+ if (_selected is not null)
+ {
+ var selected = _selected.Value.entry;
+
+ Text(selected.Title ?? selected.Id);
+ SameLine();
+ TextColored(*GetStyleColorVec4(ImGuiCol.TextLink), selected.Version.ToString());
+ Separator();
+ if (BeginTabBar("##PackageViewTabs"))
+ {
+ if (BeginTabItem("Description"))
+ {
+ TextWrapped(selected.Description ?? "Nothing.");
+ EndTabItem();
+ }
+
+ if (BeginTabItem("Details"))
+ {
+ Text("Pulled from");
+ SameLine();
+ var url = _selected.Value.client.ToString();
+ TextLinkOpenURL(url, url);
+
+ if (selected.Authors is not null)
+ {
+ Text("Authors:");
+ SameLine();
+ TextWrapped(selected.Authors.Author);
+ }
+
+ if (selected.TotalDownloads.HasValue)
+ {
+ Text("Total downloads:");
+ SameLine();
+ TextColored(*GetStyleColorVec4(ImGuiCol.TextLink), selected.TotalDownloads.ToString());
+ }
+ if (!string.IsNullOrEmpty(selected.ProjectUrl))
+ TextLinkOpenURL("Project URL", selected.ProjectUrl);
+
+ EndTabItem();
+ }
+
+ EndTabBar();
+ }
+ }
+
+ EndChild();
+
+ if (_selected is not null)
+ {
+ var selected = _selected.Value.entry;
+
+ var installed = _packages.ContainsKey(selected.Id);
+ if (Button(installed ? "Uninstall" : "Install"))
+ {
+ _packages = installed
+ ? _packages.Remove(selected.Id)
+ : _packages.Add(selected.Id, new(selected.Version));
+
+ Save();
+ }
+ }
+
+ EndGroup();
+ }
+
+ private async Task RefreshAsync()
+ {
+ _searchResults = null;
+
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ await foreach (var source in _sources)
+ {
+ try
+ {
+ var result = await source.SearchPackagesAsync(_searchQuery, take: 1000, packageType: "CringePlugin");
+
+ builder.Add(source, result);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Failed to get packages from source {Source}", source);
+ }
+ }
+
+ _searchResults = builder.ToImmutable();
+ }
+
+ private void Save()
+ {
+ _changed = true;
+
+ using var stream = File.Create(_configPath);
+
+ JsonSerializer.Serialize(stream, _packagesConfig with
+ {
+ Packages = [.._packages.Select(b => new PackageReference(b.Key, b.Value))]
+ }, NuGetClient.SerializerOptions);
+ }
+}
\ No newline at end of file
diff --git a/NuGet.config b/NuGet.config
index 3be5aa7..64fa657 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/NuGet/Converters/PackageAuthorsJsonConverter.cs b/NuGet/Converters/PackageAuthorsJsonConverter.cs
new file mode 100644
index 0000000..a186234
--- /dev/null
+++ b/NuGet/Converters/PackageAuthorsJsonConverter.cs
@@ -0,0 +1,54 @@
+using System.Collections.Immutable;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NuGet.Models;
+
+namespace NuGet.Converters;
+
+public class PackageAuthorsJsonConverter : JsonConverter
+{
+ public override PackageAuthors? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.String:
+ {
+ var author = reader.GetString()!;
+ return new PackageAuthors(author, [author]);
+ }
+ case JsonTokenType.StartArray:
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ builder.Add(reader.GetString()!);
+ }
+
+ return new PackageAuthors(string.Join(", ", builder), builder.ToImmutable());
+ }
+ case JsonTokenType.Null:
+ return null;
+ default:
+ throw new JsonException("String or array of strings expected");
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, PackageAuthors value, JsonSerializerOptions options)
+ {
+ if (value.Authors.Length == 1)
+ {
+ writer.WriteStringValue(value.Author);
+ return;
+ }
+
+ writer.WriteStartArray();
+
+ foreach (var author in value.Authors)
+ {
+ writer.WriteStringValue(author);
+ }
+
+ writer.WriteEndArray();
+ }
+}
\ No newline at end of file
diff --git a/NuGet/Converters/StringOrStringArrayConverter.cs b/NuGet/Converters/StringOrStringArrayConverter.cs
new file mode 100644
index 0000000..18070c1
--- /dev/null
+++ b/NuGet/Converters/StringOrStringArrayConverter.cs
@@ -0,0 +1,48 @@
+using System.Collections.Immutable;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace NuGet.Converters;
+
+public class StringOrStringArrayConverter : JsonConverter>
+{
+ public override ImmutableArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.String:
+ return [reader.GetString()!];
+ case JsonTokenType.StartArray:
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ builder.Add(reader.GetString()!);
+ }
+
+ return builder.ToImmutable();
+ }
+ default:
+ throw new JsonException("String or array of strings expected");
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, ImmutableArray value, JsonSerializerOptions options)
+ {
+ if (value.Length == 1)
+ {
+ writer.WriteStringValue(value[0]);
+ return;
+ }
+
+ writer.WriteStartArray();
+
+ foreach (var author in value)
+ {
+ writer.WriteStringValue(author);
+ }
+
+ writer.WriteEndArray();
+ }
+}
\ No newline at end of file
diff --git a/NuGet/Deps/DependenciesManifest.cs b/NuGet/Deps/DependenciesManifest.cs
index b726c4b..e45eb97 100644
--- a/NuGet/Deps/DependenciesManifest.cs
+++ b/NuGet/Deps/DependenciesManifest.cs
@@ -88,10 +88,10 @@ public class DependencyManifestBuilder(DirectoryInfo cacheDirectory, PackageSour
private async Task MapCatalogEntryAsync(CatalogEntry catalogEntry, NuGetFramework targetFramework,
ImmutableDictionary.Builder targets)
{
- if (targets.ContainsKey(new(catalogEntry.Id, catalogEntry.Version)))
+ if (targets.ContainsKey(new(catalogEntry.Id, catalogEntry.Version)) || !catalogEntry.DependencyGroups.HasValue)
return;
- var nearest = NuGetFrameworkUtility.GetNearest(catalogEntry.DependencyGroups, targetFramework,
+ var nearest = NuGetFrameworkUtility.GetNearest(catalogEntry.DependencyGroups.Value, targetFramework,
group => group.TargetFramework);
if (nearest is null)
@@ -103,9 +103,18 @@ public class DependencyManifestBuilder(DirectoryInfo cacheDirectory, PackageSour
foreach (var dependency in nearest.Dependencies ?? [])
{
var client = await packageSources.GetClientAsync(dependency.Id);
- var (url, entry) = await client.GetPackageRegistrationAsync(dependency.Id, versionResolver(dependency)!);
+ var registrationRoot = await client.GetPackageRegistrationRootAsync(dependency.Id);
- entry ??= await client.GetPackageCatalogEntryAsync(url);
+ var version = versionResolver(dependency)!;
+ var entry = registrationRoot.Items.SelectMany(b => b.Items ?? []).FirstOrDefault(b => b.CatalogEntry.Version == version)?.CatalogEntry;
+
+ if (entry is null)
+ {
+ var (url, sleetEntry) = await client.GetPackageRegistrationAsync(dependency.Id, versionResolver(dependency)!);
+
+ entry = sleetEntry;
+ entry ??= await client.GetPackageCatalogEntryAsync(url);
+ }
await MapCatalogEntryAsync(entry, targetFramework, targets);
}
diff --git a/NuGet/Models/CatalogEntry.cs b/NuGet/Models/CatalogEntry.cs
index b36d143..5c10e6e 100644
--- a/NuGet/Models/CatalogEntry.cs
+++ b/NuGet/Models/CatalogEntry.cs
@@ -3,7 +3,7 @@ using NuGet.Versioning;
namespace NuGet.Models;
-public record CatalogEntry(string Id, NuGetVersion Version, ImmutableArray DependencyGroups, ImmutableArray? PackageTypes,
+public record CatalogEntry(string Id, NuGetVersion Version, ImmutableArray? DependencyGroups, ImmutableArray? PackageTypes,
ImmutableArray? PackageEntries);
public record CatalogPackageEntry(string Name, string FullName, long CompressedLength, long Length);
\ No newline at end of file
diff --git a/NuGet/Models/PackageAuthors.cs b/NuGet/Models/PackageAuthors.cs
new file mode 100644
index 0000000..08f9332
--- /dev/null
+++ b/NuGet/Models/PackageAuthors.cs
@@ -0,0 +1,8 @@
+using System.Collections.Immutable;
+using System.Text.Json.Serialization;
+using NuGet.Converters;
+
+namespace NuGet.Models;
+
+[JsonConverter(typeof(PackageAuthorsJsonConverter))]
+public record PackageAuthors(string Author, ImmutableArray Authors);
\ 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/RegistrationPage.cs b/NuGet/Models/RegistrationPage.cs
index 4626929..5204f0e 100644
--- a/NuGet/Models/RegistrationPage.cs
+++ b/NuGet/Models/RegistrationPage.cs
@@ -2,4 +2,4 @@
namespace NuGet.Models;
-public record RegistrationPage(int Count, NuGetVersion Lower, NuGetVersion Upper, RegistrationEntry[] Items);
\ No newline at end of file
+public record RegistrationPage(int Count, NuGetVersion Lower, NuGetVersion Upper, RegistrationEntry[]? Items);
\ No newline at end of file
diff --git a/NuGet/Models/SearchResult.cs b/NuGet/Models/SearchResult.cs
new file mode 100644
index 0000000..0d27b0d
--- /dev/null
+++ b/NuGet/Models/SearchResult.cs
@@ -0,0 +1,33 @@
+using System.Collections.Immutable;
+using System.Text.Json.Serialization;
+using NuGet.Converters;
+using NuGet.Versioning;
+
+namespace NuGet.Models;
+
+public record SearchResult(int TotalHits, [property: JsonPropertyName("data")] ImmutableArray Entries);
+
+public record SearchResultEntry(
+ string Id,
+ NuGetVersion Version,
+ string? Description,
+ ImmutableArray Versions,
+ PackageAuthors? Authors,
+ string? IconUrl,
+ string? LicenseUrl,
+ [property: JsonConverter(typeof(StringOrStringArrayConverter))]
+ ImmutableArray? Owners,
+ string? ProjectUrl,
+ Uri Registration,
+ string? Summary,
+ [property: JsonConverter(typeof(StringOrStringArrayConverter))]
+ ImmutableArray? Tags,
+ string? Title,
+ int? TotalDownloads,
+ ImmutableArray PackageTypes,
+ bool Verified = false);
+
+public record SearchResultPackageVersion(
+ NuGetVersion Version,
+ int Downloads,
+ [property: JsonPropertyName("@id")] Uri Registration);
diff --git a/NuGet/NuGetClient.cs b/NuGet/NuGetClient.cs
index 7d4fc80..7d9c1aa 100644
--- a/NuGet/NuGetClient.cs
+++ b/NuGet/NuGetClient.cs
@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
+using System.Web;
using NuGet.Converters;
using NuGet.Models;
using NuGet.Versioning;
@@ -9,9 +10,11 @@ namespace NuGet;
public class NuGetClient
{
+ private readonly Uri _index;
private readonly HttpClient _client;
private readonly Uri _packageBaseAddress;
private readonly Uri _registration;
+ private readonly Uri _search;
public static JsonSerializerOptions SerializerOptions { get; } = new(JsonSerializerDefaults.Web)
{
@@ -24,11 +27,13 @@ public class NuGetClient
WriteIndented = true
};
- private NuGetClient(HttpClient client, Uri packageBaseAddress, Uri registration)
+ private NuGetClient(Uri index, HttpClient client, Uri packageBaseAddress, Uri registration, Uri search)
{
+ _index = index;
_client = client;
_packageBaseAddress = packageBaseAddress;
_registration = registration;
+ _search = search;
}
public Task GetPackageContentStreamAsync(string id, NuGetVersion version)
@@ -61,6 +66,37 @@ public class NuGetClient
return _client.GetFromJsonAsync(url, SerializerOptions)!;
}
+ public Task SearchPackagesAsync(string? query = null, int? skip = null, int? take = null,
+ bool? includePrerelease = null, NuGetVersion? minVersion = null, string? packageType = null)
+ {
+ var queryParameters = HttpUtility.ParseQueryString(string.Empty);
+
+ if (!string.IsNullOrEmpty(query))
+ queryParameters.Add("q", query);
+
+ if (skip.HasValue)
+ queryParameters.Add("skip", skip.Value.ToString());
+
+ if (take.HasValue)
+ queryParameters.Add("take", take.Value.ToString());
+
+ if (includePrerelease.HasValue)
+ queryParameters.Add("prerelease", includePrerelease.Value.ToString());
+
+ if (minVersion is not null)
+ queryParameters.Add("semVerLevel", minVersion.ToString());
+
+ if (!string.IsNullOrEmpty(packageType))
+ queryParameters.Add("packageType", packageType);
+
+ var builder = new UriBuilder(_search)
+ {
+ Query = queryParameters.ToString()
+ };
+
+ return _client.GetFromJsonAsync(builder.Uri, SerializerOptions)!;
+ }
+
public static async Task CreateFromIndexUrlAsync(string indexUrl)
{
var client = new HttpClient(new HttpClientHandler
@@ -71,13 +107,20 @@ public class NuGetClient
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");
+ var (registration, _, _) = index.Resources.First(b => b.Type.Id == "RegistrationsBaseUrl");
+ var (search, _, _) = index.Resources.First(b => b.Type.Id == "SearchQueryService");
if (!packageBaseAddress.EndsWith('/'))
packageBaseAddress += '/';
if (!registration.EndsWith('/'))
registration += '/';
-
- return new NuGetClient(client, new Uri(packageBaseAddress), new Uri(registration));
+
+ return new NuGetClient(new Uri(indexUrl), client, new Uri(packageBaseAddress), new Uri(registration), new Uri(search));
}
+
+ public override string ToString() => _index.ToString();
+
+ public override bool Equals(object? obj) => obj is NuGetClient { _index: { } index } && index == _index;
+
+ public override int GetHashCode() => _index.GetHashCode();
}
\ No newline at end of file
diff --git a/NuGet/PackageSourceMapping.cs b/NuGet/PackageSourceMapping.cs
index 4d22b8f..b0ab0de 100644
--- a/NuGet/PackageSourceMapping.cs
+++ b/NuGet/PackageSourceMapping.cs
@@ -1,4 +1,7 @@
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
namespace NuGet;
@@ -11,7 +14,15 @@ public class PackageSourceMapping(ImmutableArray sources)
];
public Task GetClientAsync(string packageId) =>
- _clients.FirstOrDefault(b => packageId.StartsWith(b.pattern)).client;
+ _clients.FirstOrDefault(b => Regex.IsMatch(packageId, b.pattern)).client;
+
+ public ConfiguredCancelableAsyncEnumerable.Enumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ {
+ return _clients.ToAsyncEnumerable()
+ .SelectAwait(b => new ValueTask(b.client))
+ .WithCancellation(cancellationToken)
+ .GetAsyncEnumerator();
+ }
}
-public record PackageSource(string Pattern, string Url);
\ No newline at end of file
+public record PackageSource([StringSyntax("Regex")] string Pattern, [StringSyntax("Uri")] string Url);
\ No newline at end of file