aboutsummaryrefslogtreecommitdiff
path: root/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver
diff options
context:
space:
mode:
authorAlex Pooley (@zuedev) <zuedev@gmail.com>2026-06-11 23:50:51 +0100
committerAlex Pooley (@zuedev) <zuedev@gmail.com>2026-06-11 23:50:51 +0100
commit313604645ca43aeed156e19d3d8b7d0e5d394f3d (patch)
treeb675de907eedf1e4f5bce24aff570460017587ae /Packages/com.vrchat.core.vpm-resolver/Editor/Resolver
parenta08432bcb9f28f06887fbdfac24b5757afeaf89f (diff)
downloadVRCog-313604645ca43aeed156e19d3d8b7d0e5d394f3d.tar
VRCog-313604645ca43aeed156e19d3d8b7d0e5d394f3d.tar.gz
VRCog-313604645ca43aeed156e19d3d8b7d0e5d394f3d.tar.bz2
VRCog-313604645ca43aeed156e19d3d8b7d0e5d394f3d.tar.xz
VRCog-313604645ca43aeed156e19d3d8b7d0e5d394f3d.zip
cleanup
Diffstat (limited to 'Packages/com.vrchat.core.vpm-resolver/Editor/Resolver')
-rw-r--r--Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs197
-rw-r--r--Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta3
-rw-r--r--Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs369
-rw-r--r--Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta11
4 files changed, 580 insertions, 0 deletions
diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs
new file mode 100644
index 0000000..e3a23f9
--- /dev/null
+++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Serilog;
+using Serilog.Sinks.Unity3D;
+using UnityEditor;
+using UnityEngine;
+using VRC.PackageManagement.Core;
+using VRC.PackageManagement.Core.Types;
+using VRC.PackageManagement.Core.Types.Packages;
+using Version = VRC.PackageManagement.Core.Types.VPMVersion.Version;
+
+namespace VRC.PackageManagement.Resolver
+{
+
+ [InitializeOnLoad]
+ public class Resolver
+ {
+ private const string _projectLoadedKey = "PROJECT_LOADED";
+
+ private static string _projectDir;
+ public static string ProjectDir
+ {
+ get
+ {
+ if (_projectDir != null)
+ {
+ return _projectDir;
+ }
+
+ try
+ {
+ _projectDir = new DirectoryInfo(Assembly.GetExecutingAssembly().Location).Parent.Parent.Parent
+ .FullName;
+ return _projectDir;
+ }
+ catch (Exception)
+ {
+ return "";
+ }
+ }
+ }
+
+ static Resolver()
+ {
+ SetupLogging();
+ if (!SessionState.GetBool(_projectLoadedKey, false))
+ {
+#pragma warning disable 4014
+ CheckResolveNeeded();
+#pragma warning restore 4014
+ }
+ }
+
+ private static void SetupLogging()
+ {
+ VRCLibLogger.SetLoggerDirectly(
+ new LoggerConfiguration()
+ .MinimumLevel.Information()
+ .WriteTo.Unity3D()
+ .CreateLogger()
+ );
+ }
+
+ private static async Task CheckResolveNeeded()
+ {
+ SessionState.SetBool(_projectLoadedKey, true);
+
+ //Wait for project to finish compiling
+ while (EditorApplication.isCompiling || EditorApplication.isUpdating)
+ {
+ await Task.Delay(250);
+ }
+
+ try
+ {
+
+ if (string.IsNullOrWhiteSpace(ProjectDir))
+ {
+ return;
+ }
+
+ if (VPMProjectManifest.ResolveIsNeeded(ProjectDir))
+ {
+ Debug.Log($"Resolve needed.");
+ var result = EditorUtility.DisplayDialog("VRChat Package Management",
+ $"This project requires some VRChat Packages which are not in the project yet.\n\nPress OK to download and install them.",
+ "OK", "Show Me What's Missing");
+ if (result)
+ {
+ ResolveStatic(ProjectDir);
+ }
+ else
+ {
+ ResolverWindow.ShowWindow();
+ }
+ }
+ }
+ catch (Exception)
+ {
+ // Unity says we can't open windows from this function so it throws an exception but also works fine.
+ }
+ }
+
+ public static bool VPMManifestExists()
+ {
+ return VPMProjectManifest.Exists(ProjectDir, out _);
+ }
+
+ public static void CreateManifest()
+ {
+ VPMProjectManifest.Load(ProjectDir);
+ ResolverWindow.Refresh().ConfigureAwait(false);
+ }
+
+ public static void ResolveManifest()
+ {
+ ResolveStatic(ProjectDir);
+ }
+
+ public static void ResolveStatic(string dir)
+ {
+ // Todo: calculate and show actual progress
+ EditorUtility.DisplayProgressBar($"Getting all VRChat Packages", "Downloading and Installing...", 0.5f);
+ VPMProjectManifest.Resolve(ProjectDir);
+ EditorUtility.ClearProgressBar();
+ ForceRefresh();
+ }
+
+ public static List<string> GetAllVersionsOf(string id)
+ {
+ var project = new UnityProject(ProjectDir);
+
+ var versions = new List<string>();
+ foreach (var provider in Repos.GetAll)
+ {
+ var packagesWithVersions = provider.GetAllWithVersions();
+
+ foreach (var packageVersionList in packagesWithVersions)
+ {
+ foreach (var package in packageVersionList.Value.VersionsDescending)
+ {
+ if (package.Id != id)
+ continue;
+ if (Version.TryParse(package.Version, out var result))
+ {
+ if (!versions.Contains(package.Version))
+ versions.Add(package.Version);
+ }
+ }
+ }
+ }
+
+ // Sort packages in project to the top
+ var sorted = from entry in versions orderby project.VPMProvider.HasPackage(entry) descending select entry;
+
+ return sorted.ToList<string>();
+ }
+
+ public static List<string> GetAffectedPackageList(IVRCPackage package)
+ {
+ List<string> list = new List<string>();
+
+ var project = new UnityProject(ProjectDir);
+
+ if (Repos.GetAllDependencies(package, out Dictionary<string, string> dependencies, null))
+ {
+ foreach (KeyValuePair<string, string> item in dependencies)
+ {
+ project.VPMProvider.Refresh();
+ if (project.VPMProvider.GetPackage(item.Key, item.Value) == null)
+ {
+ IVRCPackage d = Repos.GetPackageWithVersionMatch(item.Key, item.Value);
+ if (d != null)
+ {
+ list.Add(d.Id + " " + d.Version + "\n");
+ }
+ }
+ }
+
+ return list;
+ }
+
+ return null;
+ }
+
+ public static void ForceRefresh ()
+ {
+ UnityEditor.PackageManager.Client.Resolve();
+ AssetDatabase.Refresh();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta
new file mode 100644
index 0000000..a540340
--- /dev/null
+++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f872e3586f8b4f06bab3c9facd14f6e6
+timeCreated: 1659048476 \ No newline at end of file
diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs
new file mode 100644
index 0000000..264aca6
--- /dev/null
+++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs
@@ -0,0 +1,369 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEditor.UIElements;
+using UnityEngine;
+using UnityEngine.UIElements;
+using VRC.PackageManagement.Core;
+using VRC.PackageManagement.Core.Types;
+using VRC.PackageManagement.Core.Types.Packages;
+using Version = VRC.PackageManagement.Core.Types.VPMVersion.Version;
+
+namespace VRC.PackageManagement.Resolver
+{
+ public class ResolverWindow : EditorWindow
+ {
+ // VisualElements
+ private static VisualElement _rootView;
+ private static Button _refreshButton;
+ private static Button _createButton;
+ private static Button _resolveButton;
+ private static Box _manifestInfo;
+ private static Label _manifestLabel;
+ private static Label _manifestInfoText;
+ private static VisualElement _manifestPackageList;
+ private static bool _isUpdating;
+ private static Color _colorPositive = Color.green;
+ private static Color _colorNegative = new Color(1, 0.3f, 0.3f);
+
+ const string HAS_REFRESHED_KEY = "VRC.PackageManagement.Resolver.Refreshed";
+
+ private static bool IsUpdating
+ {
+ get => _isUpdating;
+ set
+ {
+ _isUpdating = value;
+ _refreshButton.SetEnabled(!value);
+ _refreshButton.text = value ? "Refreshing..." : "Refresh";
+ _manifestLabel.text = value ? "Refreshing packages ..." : "Required Packages";
+ }
+ }
+
+
+ [MenuItem("VRChat SDK/Utilities/Package Resolver")]
+ public static void ShowWindow()
+ {
+ ResolverWindow wnd = GetWindow<ResolverWindow>();
+ wnd.titleContent = new GUIContent("Package Resolver");
+ }
+
+ public static async Task Refresh()
+ {
+ if (_rootView == null || string.IsNullOrWhiteSpace(Resolver.ProjectDir)) return;
+
+ IsUpdating = true;
+ _manifestPackageList.Clear();
+
+ // check for vpm dependencies
+ if (!Resolver.VPMManifestExists())
+ {
+ _manifestInfoText.style.display = DisplayStyle.Flex;
+ _manifestInfoText.text = "No VPM Manifest";
+ _manifestInfoText.style.color = _colorNegative;
+ }
+ else
+ {
+ _manifestInfoText.style.display = DisplayStyle.None;
+ }
+
+ var manifest = VPMProjectManifest.Load(Resolver.ProjectDir);
+ var project = await Task.Run(() => new UnityProject(Resolver.ProjectDir));
+
+ // Here is where we detect if all dependencies are installed
+ var allDependencies = manifest.locked != null && manifest.locked.Count > 0
+ ? manifest.locked
+ : manifest.dependencies;
+
+ var depList = await Task.Run(() =>
+ {
+ var results = new Dictionary<(string id, string version), (IVRCPackage package, List<string> allVersions)>();
+ foreach (var pair in allDependencies)
+ {
+ var id = pair.Key;
+ var version = pair.Value.version;
+ var package = project.VPMProvider.GetPackage(id, version);
+ results.Add((id, version), (package, Resolver.GetAllVersionsOf(id)));
+ }
+
+ var legacyPackages = project.VPMProvider.GetLegacyPackages();
+
+ results = results.Where(i => !legacyPackages.Contains(i.Key.id)).ToDictionary(i => i.Key, i => i.Value);
+
+ return results;
+ });
+
+ foreach (var dep in depList)
+ {
+
+ _manifestPackageList.Add(
+ CreateDependencyRow(
+ dep.Key.id,
+ dep.Key.version,
+ project,
+ dep.Value.package,
+ dep.Value.allVersions
+ )
+ );
+ }
+
+ IsUpdating = false;
+ }
+
+ /// <summary>
+ /// Unity calls the CreateGUI method automatically when the window needs to display
+ /// </summary>
+ private void CreateGUI()
+ {
+ ScrollView scrollView = new ScrollView()
+ {
+ horizontalScrollerVisibility = ScrollerVisibility.Hidden,
+ };
+ rootVisualElement.Add(scrollView);
+
+ _rootView = scrollView;
+ _rootView.name = "root-view";
+ _rootView.styleSheets.Add((StyleSheet)Resources.Load("ResolverWindowStyle"));
+
+ // Main Container
+ var container = new Box()
+ {
+ name = "buttons"
+ };
+ _rootView.Add(container);
+
+ // Create Button
+ if (!Resolver.VPMManifestExists())
+ {
+ _createButton = new Button(Resolver.CreateManifest)
+ {
+ text = "Create",
+ name = "create-button-base"
+ };
+ container.Add(_createButton);
+ }
+ else
+ {
+ _resolveButton = new Button(Resolver.ResolveManifest)
+ {
+ text = "Resolve All",
+ name = "resolve-button-base"
+ };
+ container.Add(_resolveButton);
+ }
+
+ // Manifest Info
+ _manifestInfo = new Box()
+ {
+ name = "manifest-info",
+ };
+ _manifestLabel = (new Label("Required Packages") { name = "manifest-header" });
+ _manifestInfo.Add(_manifestLabel);
+ _manifestInfoText = new Label();
+ _manifestInfo.Add(_manifestInfoText);
+ _manifestPackageList = new ScrollView()
+ {
+ verticalScrollerVisibility = ScrollerVisibility.Hidden,
+ };
+ _manifestPackageList.style.flexDirection = FlexDirection.Column;
+ _manifestPackageList.style.alignItems = Align.Stretch;
+ _manifestInfo.Add(_manifestPackageList);
+
+ _rootView.Add(_manifestInfo);
+
+ // Refresh Button
+ var refreshBox = new Box();
+ _refreshButton = new Button(() =>
+ {
+ // When manually refreshing - ensure package manager is also up to date
+ Resolver.ForceRefresh();
+ Refresh().ConfigureAwait(false);
+ })
+ {
+ text = "Refresh",
+ name = "refresh-button-base"
+ };
+ refreshBox.Add(_refreshButton);
+ _rootView.Add(refreshBox);
+
+ // Refresh on open
+ // Sometimes unity can get into a bad state where calling package manager refresh will endlessly reload assemblies
+ // That in turn means that a Full refresh will be called every single time assemblies are loaded
+ // Which locks up the editor in an endless loop
+ // This condition ensures that the UPM resolve only happens on first launch
+ // We also call it after installing packages or hitting Refresh manually
+ if (!SessionState.GetBool(HAS_REFRESHED_KEY, false))
+ {
+ SessionState.SetBool(HAS_REFRESHED_KEY, true);
+ Resolver.ForceRefresh();
+ }
+
+ rootVisualElement.schedule.Execute(() => Refresh().ConfigureAwait(false)).ExecuteLater(100);
+ }
+
+ private static VisualElement CreateDependencyRow(string id, string version, UnityProject project, IVRCPackage package, List <string> allVersions)
+ {
+ bool havePackage = package != null;
+
+ // Table Row
+ VisualElement row = new Box { name = "package-row" };
+ VisualElement column1 = new Box { name = "package-box" };
+ VisualElement column2 = new Box { name = "package-box" };
+ VisualElement column3 = new Box { name = "package-box" };
+ VisualElement column4 = new Box { name = "package-box" };
+
+ column1.style.minWidth = 200;
+ column1.style.width = new StyleLength(new Length(40, LengthUnit.Percent));
+ column2.style.minWidth = 100;
+ column2.style.width = new StyleLength(new Length(19f, LengthUnit.Percent));
+ column3.style.minWidth = 100;
+ column3.style.width = new StyleLength(new Length(19f, LengthUnit.Percent));
+ column4.style.minWidth = 100;
+ column4.style.width = new StyleLength(new Length(19f, LengthUnit.Percent));
+
+ row.Add(column1);
+ row.Add(column2);
+ row.Add(column3);
+ row.Add(column4);
+
+ // Package Name + Status
+ column1.style.alignItems = Align.FlexStart;
+ if (havePackage)
+ {
+ column1.style.flexDirection = FlexDirection.Column;
+ var titleRow = new VisualElement();
+ titleRow.style.unityFontStyleAndWeight = FontStyle.Bold;
+ titleRow.Add(new Label(package.Title));
+ column1.Add(titleRow);
+ }
+ TextElement text = new TextElement { text = $"{id} {version} " };
+
+ column1.Add(text);
+
+ if (!havePackage)
+ {
+ TextElement missingText = new TextElement { text = "MISSING" };
+ missingText.style.color = _colorNegative;
+ column2.Add(missingText);
+ }
+
+ // Version Popup
+ var currVersion = Mathf.Max(0, havePackage ? allVersions.IndexOf(package.Version) : 0);
+ var popupField = new PopupField<string>(allVersions, 0)
+ {
+ value = allVersions[currVersion],
+ style = { flexGrow = 1}
+ };
+
+ column3.Add(popupField);
+
+ // Button
+
+ Button updateButton = new Button() { text = "Update" };
+ if (havePackage)
+ RefreshUpdateButton(updateButton, version, allVersions[0]);
+ else
+ RefreshMissingButton(updateButton);
+
+ updateButton.clicked += (() =>
+ {
+ IVRCPackage package = Repos.GetPackageWithVersionMatch(id, popupField.value);
+
+ // Check and warn on Dependencies if Updating or Downgrading
+ if (Version.TryParse(version, out var currentVersion) &&
+ Version.TryParse(popupField.value, out var newVersion))
+ {
+ Dictionary<string, string> dependencies = new Dictionary<string, string>();
+ StringBuilder dialogMsg = new StringBuilder();
+ List<string> affectedPackages = Resolver.GetAffectedPackageList(package);
+ for (int v = 0; v < affectedPackages.Count; v++)
+ {
+ dialogMsg.Append(affectedPackages[v]);
+ }
+
+ if (affectedPackages.Count > 1)
+ {
+ dialogMsg.Insert(0, "This will update multiple packages:\n\n");
+ dialogMsg.AppendLine("\nAre you sure?");
+ if (EditorUtility.DisplayDialog("Package Has Dependencies", dialogMsg.ToString(), "OK", "Cancel"))
+ OnUpdatePackageClicked(project, package);
+ }
+ else
+ {
+ OnUpdatePackageClicked(project, package);
+ }
+ }
+
+ });
+ column4.Add(updateButton);
+
+ popupField.RegisterCallback<ChangeEvent<string>>((evt) =>
+ {
+ if (havePackage)
+ RefreshUpdateButton(updateButton, version, evt.newValue);
+ else
+ RefreshMissingButton(updateButton);
+ });
+
+ return row;
+ }
+
+ private static void RefreshUpdateButton(Button button, string currentVersion, string highestAvailableVersion)
+ {
+ if (currentVersion == highestAvailableVersion)
+ {
+ button.style.display = DisplayStyle.None;
+ }
+ else
+ {
+ button.style.display = (_isUpdating ? DisplayStyle.None : DisplayStyle.Flex);
+ if (Version.TryParse(currentVersion, out var currentVersionObject) &&
+ Version.TryParse(highestAvailableVersion, out var highestAvailableVersionObject))
+ {
+ if (currentVersionObject < highestAvailableVersionObject)
+ {
+ SetButtonColor(button, _colorPositive);
+ button.text = "Update";
+ }
+ else
+ {
+ SetButtonColor(button, _colorNegative);
+ button.text = "Downgrade";
+ }
+ }
+ }
+ }
+
+ private static void RefreshMissingButton(Button button)
+ {
+ button.text = "Resolve";
+ SetButtonColor(button, Color.white);
+ }
+
+ private static void SetButtonColor(Button button, Color color)
+ {
+ button.style.color = color;
+ color.a = 0.25f;
+ button.style.borderRightColor =
+ button.style.borderLeftColor =
+ button.style.borderTopColor =
+ button.style.borderBottomColor =
+ color;
+ }
+
+ private static async void OnUpdatePackageClicked(UnityProject project, IVRCPackage package)
+ {
+ _isUpdating = true;
+ await Refresh();
+ await Task.Delay(500);
+ project.UpdateVPMPackage(package);
+ _isUpdating = false;
+ await Refresh();
+ Resolver.ForceRefresh();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta
new file mode 100644
index 0000000..fbfb6ef
--- /dev/null
+++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 32d2636186ee0834fa1dc2287750dd32
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: