A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEditor;
4using UnityEditor.PackageManager;
5using UnityEditor.PackageManager.Requests;
6using UnityEngine;
7
8namespace Unity.Multiplayer.Center
9{
10 internal static class PackageManagement
11 {
12 static PackageInstaller s_Installer;
13
14 /// <summary>
15 /// Opens the package manager window with selected package name and hides error
16 /// </summary>
17 public static void OpenPackageManager(string packageName)
18 {
19 try
20 {
21 UnityEditor.PackageManager.UI.Window.Open(packageName);
22 }
23 catch (Exception)
24 {
25 // Hide the error in the PackageManager API until the team fixes it
26 // Debug.Log("Error opening Package Manager: " + e.Message);
27 }
28 }
29
30 /// <summary>
31 /// Checks if the package is a direct dependency of the project
32 /// </summary>
33 /// <param name="packageId">The package name/id e.g. com.unity.netcode</param>
34 /// <returns>True if the package is a direct dependency</returns>
35 public static bool IsDirectDependency(string packageId)
36 {
37 var package = GetInstalledPackage(packageId);
38 return package != null && package.isDirectDependency;
39 }
40
41 /// <summary>
42 /// Checks if a package is installed.
43 /// </summary>
44 /// <param name="packageId">The package name, e.g. com.unity.netcode</param>
45 /// <returns>True if the package is installed, false otherwise</returns>
46 public static bool IsInstalled(string packageId) => GetInstalledPackage(packageId) != null;
47
48 /// <summary>
49 /// Checks if a package is embedded, linked locally, installed via Git or local Tarball.
50 /// </summary>
51 /// <param name="packageId">The package name, e.g. com.unity.netcode</param>
52 /// <returns>True if the package is linked locally, false otherwise</returns>
53 public static bool IsLinkedLocallyOrEmbeddedOrViaGit(string packageId) =>
54 GetInstalledPackage(packageId) is { source: PackageSource.Embedded or PackageSource.Local or PackageSource.Git or PackageSource.LocalTarball };
55
56 /// <summary>
57 /// Finds the installed package with the given packageId or returns null.
58 /// </summary>
59 /// <param name="packageId">The package name/id e.g. com.unity.netcode</param>
60 /// <returns>The package info</returns>
61 public static UnityEditor.PackageManager.PackageInfo GetInstalledPackage(string packageId)
62 {
63 return UnityEditor.PackageManager.PackageInfo.FindForPackageName(packageId);
64 }
65
66 /// <summary>
67 /// Filters out the packages that are already embedded, linked locally, installed via Git or local Tarball and returns this new list.
68 /// </summary>
69 /// <param name="installCandidates">A list of package IDs that are candidates for installation.</param>
70 /// <returns>A new filtered list of packages.</returns>
71 public static IEnumerable<string> RemoveLocallyLinkedOrEmbeddedOrViaGitPackagesFromList(IEnumerable<string> installCandidates)
72 {
73 var filteredList = new List<string>();
74
75 foreach (var packageId in installCandidates)
76 {
77 if (!IsLinkedLocallyOrEmbeddedOrViaGit(packageId))
78 {
79 filteredList.Add(packageId);
80 }
81 else
82 {
83 Debug.Log($"Removing {packageId} from install candidates.\n" +
84 "This package is already embedded, linked locally, installed via Git, or from a local tarball. " +
85 "Please check the Package Manager for more information or to upgrade manually.");
86 }
87 }
88
89 return filteredList;
90 }
91
92 /// <summary>
93 /// Returns true if any of the given packageIds is installed.
94 /// </summary>
95 /// <param name="packageIds">List of package is e.g com.unity.netcode</param>
96 /// <returns>True if any package is installed, false otherwise</returns>
97 public static bool IsAnyPackageInstalled(params string[] packageIds)
98 {
99 var installedPackages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
100 var hashset = new HashSet<string>();
101
102 foreach (var package in installedPackages)
103 {
104 hashset.Add(package.name);
105 }
106
107 foreach (var packageId in packageIds)
108 {
109 if (hashset.Contains(packageId))
110 {
111 return true;
112 }
113 }
114
115 return false;
116 }
117
118 /// <summary>
119 /// Installs a single package and invokes the callback when the package is installed/when the install failed.
120 /// </summary>
121 /// <param name="packageId">The package name/id e.g. com.unity.netcode</param>
122 /// <param name="onInstalled">The callback</param>
123 public static void InstallPackage(string packageId, Action<bool> onInstalled = null)
124 {
125 s_Installer = new PackageInstaller(packageId);
126 s_Installer.OnInstalled += onInstalled;
127 s_Installer.OnInstalled += _ => s_Installer = null;
128 }
129
130 /// <summary>
131 /// Register to an existing installation callback. This has no effect if no installation is ongoing (check
132 /// <see cref="IsInstallationFinished"/> to see if that is the case).
133 /// </summary>
134 /// <param name="onInstalled">The callback</param>
135 public static void RegisterToExistingInstaller(Action<bool> onInstalled)
136 {
137 if (s_Installer != null)
138 {
139 s_Installer.OnInstalled += onInstalled;
140 }
141 }
142
143 /// <summary>
144 /// Installs several packages and invokes the callback when all packages are installed/when the installation failed.
145 /// </summary>
146 /// <param name="packageIds">The package names/ids e.g. com.unity.netcode</param>
147 /// <param name="onAllInstalled">The callback</param>
148 /// <param name="packageIdsToRemove">Optional package name/ids to remove</param>
149 public static void InstallPackages(IEnumerable<string> packageIds, Action<bool> onAllInstalled = null, IEnumerable<string> packageIdsToRemove = null)
150 {
151 s_Installer = new PackageInstaller(RemoveLocallyLinkedOrEmbeddedOrViaGitPackagesFromList(packageIds), packageIdsToRemove);
152 s_Installer.OnInstalled += onAllInstalled;
153 s_Installer.OnInstalled += _ => s_Installer = null;
154 }
155
156 /// <summary>
157 /// Create a dictionary with package names as keys and versions as values
158 /// </summary>
159 /// <returns>The mapping (package id, installed version) </returns>
160 internal static Dictionary<string, string> InstalledPackageDictionary()
161 {
162 var installedPackages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
163 var installedPackageDictionary = new Dictionary<string, string>();
164
165 foreach (var package in installedPackages)
166 {
167 var splitPackageId = package.packageId.Split('@');
168 if (splitPackageId.Length == 2)
169 {
170 installedPackageDictionary[splitPackageId[0]] = splitPackageId[1];
171 }
172 }
173
174 return installedPackageDictionary;
175 }
176
177 internal class VersionChecker
178 {
179 SearchRequest m_Request;
180 public VersionChecker(string packageID)
181 {
182 m_Request = Client.Search(packageID, false);
183 EditorApplication.update += Progress;
184 }
185
186 public event Action<UnityEditor.PackageManager.PackageInfo> OnVersionFound;
187
188 void Progress()
189 {
190 if (!m_Request.IsCompleted) return;
191
192 EditorApplication.update -= Progress;
193 var foundPackage = m_Request.Result;
194 foreach (var packageInfo in foundPackage)
195 {
196 OnVersionFound?.Invoke(packageInfo);
197 }
198 }
199 }
200
201 class PackageInstaller
202 {
203 Request m_Request;
204 string[] m_PackagesToAddIds;
205 public event Action<bool> OnInstalled;
206
207 public PackageInstaller(string packageId)
208 {
209 // Add a package to the project
210 m_Request = Client.Add(packageId);
211 m_PackagesToAddIds = new[] {packageId};
212 EditorApplication.update += Progress;
213 }
214
215 public PackageInstaller(IEnumerable<string> packageIds, IEnumerable<string> packageIdsToRemove = null)
216 {
217 var packageIdsList = new List<string>();
218 foreach (var id in packageIds)
219 {
220 packageIdsList.Add(id);
221 }
222
223 var packageIdsArray = packageIdsList.ToArray();
224
225 string[] packageIdsToRemoveArray = null;
226 if (packageIdsToRemove != null)
227 {
228 var packageIdsToRemoveList = new List<string>();
229 foreach (var id in packageIdsToRemove)
230 {
231 packageIdsToRemoveList.Add(id);
232 }
233 packageIdsToRemoveArray = packageIdsToRemoveList.ToArray();
234 }
235
236 // Add a package to the project
237 m_Request = Client.AddAndRemove(packageIdsArray, packageIdsToRemoveArray);
238 m_PackagesToAddIds = packageIdsArray;
239 EditorApplication.update += Progress;
240 }
241
242 public bool IsCompleted()
243 {
244 return m_Request == null || m_Request.IsCompleted;
245 }
246
247 void Progress()
248 {
249 if (!m_Request.IsCompleted) return;
250
251 EditorApplication.update -= Progress;
252 if (m_Request.Status == StatusCode.Success)
253 {
254 Debug.Log("Installed: " + GetInstalledPackageId());
255 }
256 else if (m_Request.Status >= StatusCode.Failure)
257 {
258 // if the request has more than one package, it will only prompt error message for one
259 // We should prompt all the failed packages
260 Debug.Log("Package installation request with selected packages: " + String.Join(", ", m_PackagesToAddIds) +
261 " failed. \n Reason: "+ m_Request.Error.message);
262 }
263
264 OnInstalled?.Invoke(m_Request.Status == StatusCode.Success);
265 }
266
267 string GetInstalledPackageId()
268 {
269 switch (m_Request)
270 {
271 case AddRequest addRequest:
272 return addRequest.Result.packageId;
273 case AddAndRemoveRequest addAndRemoveRequest:
274 var packageIds = new List<string>();
275 foreach (var packageInfo in addAndRemoveRequest.Result)
276 {
277 packageIds.Add(packageInfo.packageId);
278 }
279 return string.Join(", ", packageIds);
280 default:
281 throw new InvalidOperationException("Unknown request type");
282 }
283 }
284 }
285
286 /// <summary>
287 /// Detects if any multiplayer package is installed by checking for services and Netcode installed packages.
288 /// </summary>
289 /// <returns>True if any package was detected, False otherwise</returns>
290 public static bool IsAnyMultiplayerPackageInstalled()
291 {
292 var packagesToCheck = new []
293 {
294 "com.unity.netcode",
295 "com.unity.netcode.gameobjects",
296 "com.unity.services.multiplayer",
297 "com.unity.transport",
298 "com.unity.dedicated-server",
299 "com.unity.services.cloudcode",
300 "com.unity.multiplayer.playmode",
301 "com.unity.services.vivox"
302 // Note about "com.unity.services.core": it used to be installed only with multiplayer packages, but it is also a dependency of the analytics, which is now always installed.
303 };
304
305 foreach (var package in packagesToCheck)
306 {
307 if (IsInstalled(package))
308 {
309 return true;
310 }
311 }
312
313 return false;
314 }
315
316 /// <summary>
317 /// Checks if the installation process has finished.
318 /// </summary>
319 /// <returns>True if there is no current installer instance or installation is finished on the installer</returns>
320 public static bool IsInstallationFinished()
321 {
322 return s_Installer == null || s_Installer.IsCompleted();
323 }
324 }
325}