diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index c2974dcb3a..c3c3d9f73e 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- Clicking on the Help icon in the inspector will now redirect to the relevant documentation. (#3663) ### Changed diff --git a/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs index 193a292136..d2b22c75a1 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs @@ -1,4 +1,5 @@ using Unity.Mathematics; +using Unity.Netcode.Runtime; using UnityEngine; namespace Unity.Netcode.Components @@ -43,6 +44,7 @@ namespace Unity.Netcode.Components #pragma warning restore IDE0001 [DisallowMultipleComponent] [AddComponentMenu("Netcode/Anticipated Network Transform")] + [HelpURL(HelpUrls.AnticipatedNetworkTransform)] public class AnticipatedNetworkTransform : NetworkTransform { diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs index f6209a98b3..2fe642162a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; +using Unity.Netcode.Runtime; using UnityEngine; #if UNITY_EDITOR using UnityEditor.Animations; @@ -186,6 +187,7 @@ internal NetworkAnimatorStateChangeHandler(NetworkAnimator networkAnimator) /// NetworkAnimator enables remote synchronization of state for on network objects. /// [AddComponentMenu("Netcode/Network Animator")] + [HelpURL(HelpUrls.NetworkAnimator)] public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver { [Serializable] diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs index a157d26c63..590fad77b7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs @@ -1,4 +1,5 @@ #if COM_UNITY_MODULES_PHYSICS +using Unity.Netcode.Runtime; using UnityEngine; namespace Unity.Netcode.Components @@ -10,6 +11,7 @@ namespace Unity.Netcode.Components [RequireComponent(typeof(NetworkTransform))] [RequireComponent(typeof(Rigidbody))] [AddComponentMenu("Netcode/Network Rigidbody")] + [HelpURL(HelpUrls.NetworkRigidbody)] public class NetworkRigidbody : NetworkRigidbodyBase { diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs index f7c9e14d1e..dd84d52252 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs @@ -1,4 +1,5 @@ #if COM_UNITY_MODULES_PHYSICS2D +using Unity.Netcode.Runtime; using UnityEngine; namespace Unity.Netcode.Components @@ -10,6 +11,7 @@ namespace Unity.Netcode.Components [RequireComponent(typeof(NetworkTransform))] [RequireComponent(typeof(Rigidbody2D))] [AddComponentMenu("Netcode/Network Rigidbody 2D")] + [HelpURL(HelpUrls.NetworkRigidbody2D)] public class NetworkRigidbody2D : NetworkRigidbodyBase { public Rigidbody2D Rigidbody2D => m_InternalRigidbody2D; diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 56ab5dcb27..33fd4c523d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Text; using Unity.Mathematics; +using Unity.Netcode.Runtime; using UnityEngine; namespace Unity.Netcode.Components @@ -14,6 +15,7 @@ namespace Unity.Netcode.Components /// [DisallowMultipleComponent] [AddComponentMenu("Netcode/Network Transform")] + [HelpURL(HelpUrls.NetworkTransform)] public class NetworkTransform : NetworkBehaviour { #if UNITY_EDITOR diff --git a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs index b455bde3d6..d5dff7e069 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Unity.Collections; using Unity.Jobs; +using Unity.Netcode.Runtime; using UnityEngine; namespace Unity.Netcode.Components @@ -71,6 +72,7 @@ public interface IContactEventHandlerWithInfo : IContactEventHandler ///
/// [AddComponentMenu("Netcode/Rigidbody Contact Event Manager")] + [HelpURL(HelpUrls.RigidbodyContactEventManager)] public class RigidbodyContactEventManager : MonoBehaviour { public static RigidbodyContactEventManager Instance { get; private set; } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 3c6662b7be..5fe071ce0e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -10,6 +10,7 @@ using UnityEngine.SceneManagement; using Debug = UnityEngine.Debug; using Unity.Netcode.Components; +using Unity.Netcode.Runtime; namespace Unity.Netcode { @@ -17,6 +18,7 @@ namespace Unity.Netcode /// The main component of the library /// [AddComponentMenu("Netcode/Network Manager", -100)] + [HelpURL(HelpUrls.NetworkManager)] public class NetworkManager : MonoBehaviour, INetworkUpdateSystem { /// diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index bd9787ca8a..50101deeb7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Text; using Unity.Netcode.Components; +using Unity.Netcode.Runtime; #if UNITY_EDITOR using UnityEditor; #if UNITY_2021_2_OR_NEWER @@ -24,6 +25,7 @@ namespace Unity.Netcode /// [AddComponentMenu("Netcode/Network Object", -99)] [DisallowMultipleComponent] + [HelpURL(HelpUrls.NetworkObject)] public sealed class NetworkObject : MonoBehaviour { [HideInInspector] diff --git a/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs new file mode 100644 index 0000000000..91bf160544 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs @@ -0,0 +1,22 @@ +namespace Unity.Netcode.Runtime +{ + internal static class HelpUrls + { + private const string k_BaseUrl = "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest/?subfolder=/"; + private const string k_BaseManualUrl = k_BaseUrl + "manual/"; + private const string k_BaseApiUrl = k_BaseUrl + "api/Unity.Netcode"; + + // The HelpUrls have to be defined as public for the test to work + public const string NetworkManager = k_BaseManualUrl + "components/core/networkmanager.html"; + public const string NetworkObject = k_BaseManualUrl + "components/core/networkobject.html"; + public const string NetworkAnimator = k_BaseManualUrl + "components/helper/networkanimator.html"; + public const string NetworkRigidbody = k_BaseManualUrl + "advanced-topics/physics.html#networkrigidbody"; + public const string NetworkRigidbody2D = k_BaseManualUrl + "advanced-topics/physics.html#networkrigidbody2d"; + public const string RigidbodyContactEventManager = k_BaseApiUrl + ".Components.RigidbodyContactEventManager.html"; + public const string NetworkTransform = k_BaseManualUrl + "components/helper/networktransform.html"; + public const string AnticipatedNetworkTransform = k_BaseManualUrl + "advanced-topics/client-anticipation.html"; + public const string UnityTransport = k_BaseApiUrl + ".Transports.UTP.UnityTransport.html"; + public const string SecretsLoaderHelper = k_BaseApiUrl + ".Transports.UTP.SecretsLoaderHelper.html"; + public const string SinglePlayerTransport = k_BaseApiUrl + ".Transports.SinglePlayer.SinglePlayerTransport.html"; + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs.meta b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs.meta new file mode 100644 index 0000000000..24f9cc0f69 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5f9653e7d00a4ad0aeb89e72cfddb68a +timeCreated: 1757450738 diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs index 17a240b676..e7ebde09d8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using Unity.Netcode.Runtime; +using UnityEngine; namespace Unity.Netcode.Transports.SinglePlayer { @@ -11,6 +13,8 @@ namespace Unity.Netcode.Transports.SinglePlayer /// /// You can only start as a host when using this transport. /// + [AddComponentMenu("Netcode/Single Player Transport")] + [HelpURL(HelpUrls.SinglePlayerTransport)] public class SinglePlayerTransport : NetworkTransport { /// diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs index 40b8f2ba44..dbbfcade04 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Unity.Netcode.Runtime; using UnityEngine; namespace Unity.Netcode.Transports.UTP @@ -13,6 +14,7 @@ namespace Unity.Netcode.Transports.UTP /// - SetClientSecrets /// directly, instead of relying on this. /// + [HelpURL(HelpUrls.SecretsLoaderHelper)] public class SecretsLoaderHelper : MonoBehaviour { internal struct ServerSecrets diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs index 4340568122..e6bc0ca957 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs @@ -11,6 +11,7 @@ using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; +using Unity.Netcode.Runtime; using Unity.Networking.Transport; using Unity.Networking.Transport.Relay; using Unity.Networking.Transport.TLS; @@ -28,6 +29,7 @@ namespace Unity.Netcode.Transports.UTP /// Note: This is highly recommended to use over UNet. /// [AddComponentMenu("Netcode/Unity Transport")] + [HelpURL(HelpUrls.UnityTransport)] public partial class UnityTransport : NetworkTransport, INetworkStreamDriverConstructor { /// diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs new file mode 100644 index 0000000000..4d3a6d608f --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs @@ -0,0 +1,165 @@ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using NUnit.Framework; +using Unity.Netcode.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + internal class HelpUrlTests + { + private const string k_PackageName = "com.unity.netcode.gameobjects"; + private static readonly HttpClient k_HttpClient = new(); + + private bool m_VerboseLogging = false; + + [UnityTest] + public IEnumerator ValidateUrlsAreValid() + { + var names = new List(); + var allUrls = new List(); + + // GetFields() can only see public strings. Ensure each HelpUrl is public. + foreach (var constant in typeof(HelpUrls).GetFields()) + { + if (constant.IsLiteral && !constant.IsInitOnly) + { + names.Add(constant.Name); + allUrls.Add((string)constant.GetValue(null)); + } + } + + VerboseLog($"Found {allUrls.Count} URLs"); + + var tasks = new List>(); + foreach (var url in allUrls) + { + tasks.Add(AreUnityDocsAvailableAt(url)); + } + + while (tasks.Any(task => !task.IsCompleted)) + { + yield return new WaitForSeconds(0.01f); + } + + for (int i = 0; i < allUrls.Count; i++) + { + Assert.IsTrue(tasks[i].Result, $"HelpUrls.{names[i]} has an invalid path! Path: {allUrls[i]}"); + } + } + + private async Task AreUnityDocsAvailableAt(string url) + { + try + { + var split = url.Split('#'); + url = split[0]; + + var stream = await GetContentFromRemoteFile(url); + + var redirectUrl = CalculateRedirectURl(url, stream); + VerboseLog($"Calculated Redirect URL: {redirectUrl}"); + + var content = await GetContentFromRemoteFile(redirectUrl); + + // If original url had an anchor part (e.g. some/url.html#anchor) + if (split.Length > 1) + { + var anchorString = split[1]; + + // All headings will have an id with the anchorstring (e.g.

) + if (!content.Contains($"id=\"{anchorString}\">")) + { + return false; + } + } + + return true; + } + catch (Exception e) + { + VerboseLog(e.Message); + return false; + } + } + + /// + /// Checks if a remote file at the exists, and if access is not restricted. + /// + /// URL to a remote file. + /// True if the file at the is able to be downloaded, false if the file does not exist, or if the file is restricted. + private async Task GetContentFromRemoteFile(string url) + { + //Checking if URI is well formed is optional + var uri = new Uri(url); + if (!uri.IsWellFormedOriginalString()) + { + throw new Exception($"URL {url} is not well formed"); + } + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, uri); + using var response = await k_HttpClient.SendAsync(request); + if (!response.IsSuccessStatusCode || response.Content.Headers.ContentLength <= 0) + { + throw new Exception($"Failed to get remote file from URL {url}"); + } + + return await response.Content.ReadAsStringAsync(); + } + catch + { + throw new Exception($"URL {url} request failed"); + } + } + + private string CalculateRedirectURl(string originalRequest, string content) + { + var uri = new Uri(originalRequest); + var baseRequest = $"{uri.Scheme}://{uri.Host}"; + foreach (var segment in uri.Segments) + { + if (segment.Contains(k_PackageName)) + { + break; + } + baseRequest += segment; + } + + var subfolderRegex = new Regex(@"[?&](\w[\w.]*)=([^?&]+)").Match(uri.Query); + var subfolder = ""; + foreach (Group match in subfolderRegex.Groups) + { + subfolder = match.Value; + } + + string pattern = @"com.unity.netcode.gameobjects\@(\d+.\d+)"; + var targetDestination = ""; + foreach (Match match in Regex.Matches(content, pattern)) + { + targetDestination = match.Value; + break; + } + + return baseRequest + targetDestination + subfolder; + } + + private void VerboseLog(string message) + { + if (m_VerboseLogging) + { + Debug.unityLogger.Log(message); + } + } + + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs.meta new file mode 100644 index 0000000000..009ea8ed40 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a44ef45dca794f618ae195832470a7cd +timeCreated: 1757519683 \ No newline at end of file