diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs index 00e7719e4a..854ddd65d2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using Unity.Collections; +using Unity.Netcode.Logging; +using UnityEditor; using UnityEngine; using UnityEngine.Serialization; @@ -246,11 +248,19 @@ internal NetworkConfig Copy() /// runtime modifications to a property outside of the recommended range. /// For each property checked below, provide a brief description of the reason. /// - internal void OnValidate() + internal void OnValidate(ContextualLogger log) { // Legacy NGO versions defaulted this value to 1 second that has since been determiend // any range less than 10 seconds can lead to dropped messages during scene events. SpawnTimeout = Mathf.Clamp(SpawnTimeout, MinSpawnTimeout, MaxSpawnTimeout); + + var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); + + // If the scene is dirty and the asset database is not currently updating then we can validate the NetworkPrefabs + if (activeScene.isDirty && !EditorApplication.isUpdating) + { + Prefabs.Initialize(log); + } } /// @@ -378,26 +388,14 @@ public bool CompareConfig(ulong hash) return hash == GetConfig(); } - internal void InitializePrefabs() + internal void InitializePrefabs(ContextualLogger log) { if (HasOldPrefabList()) { MigrateOldNetworkPrefabsToNetworkPrefabsList(); } - Prefabs.Initialize(); - } - - [NonSerialized] - private bool m_DidWarnOldPrefabList = false; - - private void WarnOldPrefabList() - { - if (!m_DidWarnOldPrefabList) - { - Debug.LogWarning("Using Legacy Network Prefab List. Consider Migrating."); - m_DidWarnOldPrefabList = true; - } + Prefabs.Initialize(log); } /// diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs index c6f30d2835..114f97f9cc 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Unity.Netcode.Logging; using UnityEngine; namespace Unity.Netcode @@ -146,24 +148,37 @@ public uint TargetPrefabGlobalObjectIdHash /// True if the NetworkPrefab is valid and ready for use, false otherwise public bool Validate(int index = -1) { + var log = new ContextualLogger(); + return Validate(log, index); + } + + internal bool Validate(ContextualLogger log, int index = -1) + { + using var logContext = log.AddDisposableInfo("Invalid prefab", Prefab?.name); + NetworkObject networkObject; if (Override == NetworkPrefabOverride.None) { if (Prefab == null) { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} cannot be null ({nameof(NetworkPrefab)} at index: {index})"); + log.Warning(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} cannot be null").AddInfo($"{nameof(NetworkPrefab)} at index", index)); return false; } networkObject = Prefab.GetComponent(); if (networkObject == null) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + log.Warning(new Context(LogLevel.Error, $"Prefab is missing a {nameof(NetworkObject)} component!").AddObject(Prefab)); + return false; + } + + { + var childNetworkObjects = new List(); + Prefab.GetComponentsInChildren(true, childNetworkObjects); + if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects { - NetworkLog.LogWarning($"{NetworkPrefabHandler.PrefabDebugHelper(this)} is missing a {nameof(NetworkObject)} component (entry will be ignored)."); + log.Warning(new Context(LogLevel.Error, $"Prefab has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)").AddObject(Prefab)); } - - return false; } return true; @@ -176,14 +191,9 @@ public bool Validate(int index = -1) { if (SourceHashToOverride == 0) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(SourceHashToOverride)} is zero (entry will be ignored)."); - } - + log.Warning(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} {nameof(SourceHashToOverride)} is zero!")); return false; } - break; } case NetworkPrefabOverride.Prefab: @@ -197,20 +207,16 @@ public bool Validate(int index = -1) { SourcePrefabToOverride = Prefab; } - else if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + else { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(SourcePrefabToOverride)} is null (entry will be ignored)."); + log.Warning(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} {nameof(SourcePrefabToOverride)} is null!")); return false; } } if (!SourcePrefabToOverride.TryGetComponent(out networkObject)) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} ({SourcePrefabToOverride.name}) is missing a {nameof(NetworkObject)} component (entry will be ignored)."); - } - + log.Warning(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} is missing a {nameof(NetworkObject)} component!").AddObject(SourcePrefabToOverride)); return false; } @@ -221,21 +227,18 @@ public bool Validate(int index = -1) // Validate target prefab override values next if (OverridingTargetPrefab == null) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(OverridingTargetPrefab)} is null!"); - } - + // Safe to create context early as this code is not in any hot path + var ctx = new Context(LogLevel.Error, $"{nameof(OverridingTargetPrefab)} is null! {nameof(NetworkPrefab)} entry will be removed and ignored."); switch (Override) { case NetworkPrefabOverride.Hash: { - Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {SourceHashToOverride} will be removed and ignored."); + log.Warning(ctx.AddInfo(nameof(SourceHashToOverride), SourceHashToOverride)); break; } case NetworkPrefabOverride.Prefab: { - Debug.LogWarning($"{nameof(NetworkPrefab)} override entry ({SourcePrefabToOverride.name}) will be removed and ignored."); + log.Warning(ctx.AddInfo(nameof(SourcePrefabToOverride), SourcePrefabToOverride.name)); break; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs index 45d93ad9d7..2ef6fae5c2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Text; +using Unity.Netcode.Logging; using UnityEngine; namespace Unity.Netcode @@ -50,9 +50,15 @@ public class NetworkPrefabs [NonSerialized] private List m_RuntimeAddedPrefabs = new List(); + private ContextualLogger m_Log; + + private bool m_Initialized; + + private void AddTriggeredByNetworkPrefabList(NetworkPrefab networkPrefab) { - if (AddPrefabRegistration(networkPrefab)) + // We don't have to re-validate the prefab as the PrefabList will have validated before invoking this + if (AddPrefabRegistrationPreValidated(networkPrefab)) { // Don't add this to m_RuntimeAddedPrefabs // This prefab is now in the PrefabList, so if we shutdown and initialize again, we'll pick it up from there. @@ -79,6 +85,7 @@ private void RemoveTriggeredByNetworkPrefabList(NetworkPrefab networkPrefab) /// internal void Shutdown() { + m_Initialized = false; foreach (var list in NetworkPrefabsLists) { list.OnAdd -= AddTriggeredByNetworkPrefabList; @@ -93,27 +100,18 @@ internal void Shutdown() /// When true, logs warnings about invalid prefabs that are removed during initialization public void Initialize(bool warnInvalid = true) { + Initialize(m_Log ?? new ContextualLogger(), warnInvalid); + } + + internal void Initialize(ContextualLogger log, bool warnInvalid = true) + { + m_Log = log; m_Prefabs.Clear(); NetworkPrefabsLists.RemoveAll(x => x == null); - foreach (var list in NetworkPrefabsLists) - { - list.OnAdd += AddTriggeredByNetworkPrefabList; - list.OnRemove += RemoveTriggeredByNetworkPrefabList; - } NetworkPrefabOverrideLinks.Clear(); OverrideToNetworkPrefab.Clear(); - var prefabs = new List(); - - if (NetworkPrefabsLists.Count != 0) - { - foreach (var list in NetworkPrefabsLists) - { - prefabs.AddRange(list.PrefabList); - } - } - m_Prefabs = new List(); List removeList = null; @@ -122,15 +120,33 @@ public void Initialize(bool warnInvalid = true) removeList = new List(); } - foreach (var networkPrefab in prefabs) + foreach (var list in NetworkPrefabsLists) { - if (AddPrefabRegistration(networkPrefab)) + if (list == null) { - m_Prefabs.Add(networkPrefab); + continue; } - else + // Validate will remove any invalid items from the list + list.BuildLogger(); + + list.OnAdd += AddTriggeredByNetworkPrefabList; + list.OnRemove += RemoveTriggeredByNetworkPrefabList; + + foreach (var networkPrefab in list.List) { - removeList?.Add(networkPrefab); + if (networkPrefab == null) + { + continue; + } + + if (networkPrefab.Validate(list.Log) && AddPrefabRegistrationPreValidated(networkPrefab)) + { + m_Prefabs.Add(networkPrefab); + } + else + { + removeList?.Add(networkPrefab); + } } } @@ -149,13 +165,10 @@ public void Initialize(bool warnInvalid = true) // Clear out anything that is invalid or not used if (removeList?.Count > 0) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - var sb = new StringBuilder("Removing invalid prefabs from Network Prefab registration: "); - sb.AppendJoin(", ", removeList); - NetworkLog.LogWarning(sb.ToString()); - } + log.Warning(new Context(LogLevel.Error, "Removing invalid prefabs from Network Prefab registration")); } + + m_Initialized = true; } /// @@ -171,6 +184,12 @@ public void Initialize(bool warnInvalid = true) /// public bool Add(NetworkPrefab networkPrefab) { + if (!m_Initialized) + { + m_RuntimeAddedPrefabs.Add(networkPrefab); + return true; + } + if (AddPrefabRegistration(networkPrefab)) { m_Prefabs.Add(networkPrefab); @@ -287,43 +306,40 @@ private bool AddPrefabRegistration(NetworkPrefab networkPrefab) return false; } // Safeguard validation check since this method is called from outside of NetworkConfig and we can't control what's passed in. - if (!networkPrefab.Validate()) + if (!networkPrefab.Validate(m_Log)) { return false; } + return AddPrefabRegistrationPreValidated(networkPrefab); + } + private bool AddPrefabRegistrationPreValidated(NetworkPrefab networkPrefab) + { uint source = networkPrefab.SourcePrefabGlobalObjectIdHash; uint target = networkPrefab.TargetPrefabGlobalObjectIdHash; // Make sure the prefab isn't already registered. - if (NetworkPrefabOverrideLinks.ContainsKey(source)) + if (NetworkPrefabOverrideLinks.TryGetValue(source, out var otherPrefab)) { - var networkObject = networkPrefab.Prefab.GetComponent(); - // This should never happen, but in the case it somehow does log an error and remove the duplicate entry - Debug.LogError($"{nameof(NetworkPrefab)} ({networkObject.name}) has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} source entry value of: {source}!"); + m_Log.Error(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} has a matching {nameof(NetworkObject.GlobalObjectIdHash)} with another object. This should not happen!").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), source).AddInfo("Duplicated Object", otherPrefab.Prefab.name).AddObject(networkPrefab.Prefab)); return false; } - // If we don't have an override configured, registration is simple! - if (networkPrefab.Override == NetworkPrefabOverride.None) - { - NetworkPrefabOverrideLinks.Add(source, networkPrefab); - return true; - } - switch (networkPrefab.Override) { + case NetworkPrefabOverride.None: + { + NetworkPrefabOverrideLinks.Add(source, networkPrefab); + break; + } case NetworkPrefabOverride.Prefab: case NetworkPrefabOverride.Hash: { NetworkPrefabOverrideLinks.Add(source, networkPrefab); - if (!OverrideToNetworkPrefab.ContainsKey(target)) - { - OverrideToNetworkPrefab.Add(target, source); - } + OverrideToNetworkPrefab.TryAdd(target, source); + break; } - break; } return true; diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabsList.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabsList.cs index 5003a2fe4e..293cc56e5a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabsList.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabsList.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Unity.Netcode.Logging; using UnityEngine; using UnityEngine.Serialization; @@ -27,6 +28,10 @@ public class NetworkPrefabsList : ScriptableObject [SerializeField] internal List List = new List(); + // Need own logger as is a UnityEngine.Object + // we want the logs to point to this Object in the editor + internal ContextualLogger Log; + /// /// Read-only view into the prefabs list, enabling iterating and examining the list. /// Actually modifying the list should be done using @@ -34,6 +39,17 @@ public class NetworkPrefabsList : ScriptableObject /// public IReadOnlyList PrefabList => List; + internal void BuildLogger() + { + if (Log == null) + { + Log = new ContextualLogger(this); + Log.AddInfo(nameof(NetworkPrefabsList), name); + } + } + + private void Awake() => BuildLogger(); + /// /// Adds a prefab to the prefab list. Performing this here will apply the operation to all /// s that reference this list. @@ -41,6 +57,12 @@ public class NetworkPrefabsList : ScriptableObject /// The NetworkPrefab to add to the shared list public void Add(NetworkPrefab prefab) { + if (prefab == null || !prefab.Validate(Log)) + { + Log.Error(new Context(LogLevel.Normal, $"Failed to register {nameof(NetworkPrefab)}")); + return; + } + List.Add(prefab); OnAdd?.Invoke(prefab); } @@ -52,7 +74,10 @@ public void Add(NetworkPrefab prefab) /// The NetworkPrefab to remove from the shared list public void Remove(NetworkPrefab prefab) { - List.Remove(prefab); + if (!List.Remove(prefab)) + { + Log.Warning(new Context(LogLevel.Normal, $"Failed to remove {nameof(NetworkPrefab)}")); + } OnRemove?.Invoke(prefab); } @@ -91,5 +116,27 @@ public bool Contains(NetworkPrefab prefab) return false; } + + /// + /// Validates all the prefabs in the list and removes them from the list if not valid + /// + internal void Validate(bool doRemove = true) + { + BuildLogger(); + + for (int i = 0; i < List.Count; i++) + { + var prefab = List[i]; + + // Blank entry - This is ok + if (prefab == null) + { + continue; + } + + // Pass in local logger so any logs will highlight this list in the editor in case of an error + prefab.Validate(Log, i); + } + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 8fc97dd84e..9c99f9fb71 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -1057,7 +1057,7 @@ private void Awake() Log = new ContextualLogger(this, false); } - NetworkConfig?.InitializePrefabs(); + NetworkConfig?.InitializePrefabs(Log); UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded; #if UNITY_EDITOR @@ -1263,7 +1263,7 @@ internal void Initialize(bool server) BehaviourUpdater = new NetworkBehaviourUpdater(); BehaviourUpdater.Initialize(this); - NetworkConfig.InitializePrefabs(); + NetworkConfig.InitializePrefabs(Log); PrefabHandler.RegisterPlayerPrefab(); #if UNITY_EDITOR BeginNetworkSession(); @@ -1828,52 +1828,13 @@ internal void OnValidate() } // Do a validation pass on NetworkConfig properties - NetworkConfig.OnValidate(); + NetworkConfig.OnValidate(Log); if (GetComponentInChildren() != null) { Log.Warning(new Context(LogLevel.Normal, $"{nameof(NetworkManager)} cannot be a {nameof(NetworkObject)}.")); } - var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); - - // If the scene is not dirty or the asset database is currently updating then we can skip updating the NetworkPrefab information - if (!activeScene.isDirty || EditorApplication.isUpdating) - { - return; - } - - // During OnValidate we will always clear out NetworkPrefabOverrideLinks and rebuild it - NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.Clear(); - - var prefabs = NetworkConfig.Prefabs.Prefabs; - // Check network prefabs and assign to dictionary for quick look up - for (int i = 0; i < prefabs.Count; i++) - { - var networkPrefab = prefabs[i]; - var networkPrefabGo = networkPrefab?.Prefab; - if (networkPrefabGo == null) - { - continue; - } - - var networkObject = networkPrefabGo.GetComponent(); - if (networkObject == null) - { - Log.Warning(new Context(LogLevel.Normal, $"Cannot register prefab to {nameof(NetworkManager)}, missing a {nameof(NetworkObject)} component at its root").AddObject(networkPrefab.Prefab)); - continue; - } - - { - var childNetworkObjects = new List(); - networkPrefabGo.GetComponentsInChildren(true, childNetworkObjects); - if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects - { - Log.Warning(new Context(LogLevel.Normal, $"Prefab has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)").AddObject(networkPrefab.Prefab)); - } - } - } - try { OnValidateComponent(); diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/AGENTS.md b/com.unity.netcode.gameobjects/Runtime/Logging/AGENTS.md new file mode 100644 index 0000000000..f22c32cea6 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/AGENTS.md @@ -0,0 +1,83 @@ +# Logging + +This directory contains the structured logging system used across NGO. The supported entry point is `ContextualLogger` together with the `Context` value type. The older `if (NetworkLog.CurrentLogLevel <= LogLevel.X) NetworkLog.LogX(...)` pattern is being phased out — prefer the `Context`-based API for new code. + +## Two dimensions + +NGO logging has two **independent** dimensions, set explicitly at every call site. Severity controls how Unity *displays* the line; verbosity controls whether NGO *emits* it at all. + +| | Severity (`UnityEngine.LogType`) | Verbosity (`NetworkLog.LogLevel`) | +|---|---|---| +| Set by | `log.Info` → `Log` · `log.Warning` → `Warning` · `log.Error` → `Error` · `log.Exception` → `Exception` (bypasses verbosity) | `Context.Level`: `Developer` < `Normal` < `Error` < `None` | +| Filtered against | — (always reaches Unity if emitted) | `NetworkManager.LogLevel` threshold (`if (m_ManagerContext.LogLevel > context.Level) return;` in `ContextualLogger.Log`) | +| Scope | Per-message | Per-message tag, gated per-`NetworkManager` | + +Server-routed variants (`InfoServer` / `WarningServer` / `ErrorServer`) additionally forward the message to the server/session-owner via `LogContextNetworkManager.TrySendMessage`. + +## Creating a logger + +Before constructing a new `ContextualLogger`, check what's already available: + +1. **Does the surrounding type already have a `Log` field?** (e.g. `NetworkManager.Log`, `NetworkPrefabsList.Log`.) Use it. +2. **Do you have a `NetworkManager` reference but no obvious Unity object to attribute to?** Use `networkManager.Log` directly instead of constructing a new logger. +3. **Otherwise**, construct one with the explicit `(Object, NetworkManager)` constructor — always pass both, and cache the result as a `private` field rather than constructing per call. + +### What the constructor arguments control + +The `NetworkManager` argument decides which `LogLevel` threshold gates the logger (`LogContextNetworkManager.LogLevel`), which connection `*Server` variants route over, and which `LocalClientId` they tag as the sender. The `UnityEngine.Object` argument decides which object Unity's Console pings when the user clicks the log line — pass `this` whenever the surrounding type derives from `UnityEngine.Object`. For a one-off line about a different object, override per-message with `Context.AddObject(...)` rather than spinning up a second logger. + +The two-argument constructor is the only form that gates the logger on the right `NetworkManager`. The single-argument and parameterless constructors fall back to tracking `NetworkManager.Singleton`, which silently misroutes logs in any setup with more than one manager (integration tests, host + client in one process, distributed authority). + +### Bootstrap in `Awake`, rebind once the manager is known + +For types that derive from `UnityEngine.Object` (`MonoBehaviour`, `NetworkBehaviour`, `ScriptableObject`), the relevant `NetworkManager` usually isn't available at `Awake` time — but you still want a usable logger as soon as the object exists. + +1. **In `Awake`**, construct the logger with `new ContextualLogger(this)` so it has Console attribution immediately. This is the only sanctioned use of the singleton-tracking constructor — accept it as a transient state. +2. **As soon as the relevant `NetworkManager` is set** (`OnNetworkPreSpawn` for `NetworkBehaviour`, or wherever the manager reference lands for owned subsystems), **recreate** the logger as `new ContextualLogger(this, networkManager)`. + +Do not skip the rebind — a logger that stays on the singleton fallback is the bug this guidance exists to prevent. + +```csharp +internal class MyNetworkBehaviour : NetworkBehaviour +{ + private ContextualLogger m_Log; + + private void Awake() => m_Log = new ContextualLogger(this); + + public override void OnNetworkPreSpawn() => m_Log = new ContextualLogger(this, NetworkManager); +} +``` + +## Writing a log line + +Construct a `Context` per call: `new Context(level, message)`, optionally enriched fluently with `AddTag(name)`, `AddInfo(key, value)`, or `AddObject(unityObject)`. The calling member is captured via `[CallerMemberName]` unless suppressed. + +```csharp +log.Warning(new Context(LogLevel.Developer, "Serialized type not optimized for DA").AddTag(type.Name)); +log.Info(new Context(LogLevel.Normal, "Connection approved")); +log.Error(new Context(LogLevel.Error, "Transport handshake failed")); +``` + +**Pair severity and verbosity sensibly.** Tag info-shaped messages with `Developer` or `Normal`, never `Error` — at `LogLevel.Error` only `Warning`/`Error` severities should appear. `Warning`/`Error` may carry any verbosity (a `Warning` tagged `Developer` is a noisy diagnostic warning gated behind chatty mode). + +**Only use compile-time-constant strings in `Message`.** Interpolation runs *before* `Context` reaches the logger, so `$"…{value}…"` always allocates — even when the verbosity gate filters the line out. The only acceptable interpolation is one the compiler can fold to a `const` (literals + `nameof(...)`). Hand any runtime value to `Context` via `AddTag`/`AddInfo`/`AddObject`; the builder only runs when the gate passes. If `Context`/`LogBuilder` can't express what you need, **extend them** rather than reach for `$"…"` at the call site — that's how the API is meant to grow. + +```csharp +// Bad — interpolation runs unconditionally, before the verbosity check +log.Info(new Context(LogLevel.Developer, $"Spawned {obj.name} with id {obj.NetworkObjectId}")); + +// Good — literal + nameof folds at compile time; runtime values are added as structured info +log.Info(new Context(LogLevel.Developer, $"Spawned {nameof(NetworkObject)}").AddTag(obj.name).AddInfo(nameof(obj.NetworkObjectId), obj.NetworkObjectId)); +``` + +Never wrap a `ContextualLogger` call in `if (NetworkLog.CurrentLogLevel <= …)` — `Context.Level` is the verbosity gate. All `ContextualLogger` methods are `[Conditional("UNITY_ASSERTIONS")]`, so calls (and the `Context` allocation) compile out of release builds. + +## File map + +- `LogLevel.cs` — public verbosity enum (`Developer` / `Normal` / `Error` / `None`). +- `LogContext.cs` — `Context` value type (per-message payload: level, message, tags, info, object override). +- `ContextualLogger.cs` — the logger; combines system-wide context (`LogContextNetworkManager`, `GenericContext`) with per-call `Context` and forwards to `Debug.unityLogger`. Also handles the verbosity gate. +- `LogContextNetworkManager.cs` — system-wide context tied to a `NetworkManager` (holds the threshold, handles server/session-owner forwarding). +- `GenericContext.cs` — pooled key/value + tag store used by both per-logger and per-message context. +- `LogBuilder.cs` — pooled string assembly for the final formatted line. +- `NetworkLog.cs` — legacy static façade. Still in use; new code should prefer `ContextualLogger` directly. diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/AGENTS.md.meta b/com.unity.netcode.gameobjects/Runtime/Logging/AGENTS.md.meta new file mode 100644 index 0000000000..725c72f3c6 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/AGENTS.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 530112d190677407e8aab708838d29bd +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/CLAUDE.md b/com.unity.netcode.gameobjects/Runtime/Logging/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/CLAUDE.md.meta b/com.unity.netcode.gameobjects/Runtime/Logging/CLAUDE.md.meta new file mode 100644 index 0000000000..f648547cbc --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/CLAUDE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dcaa6e3c4a8c64791b1517fc6a503d6e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/NetworkManagerConfigurationTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/NetworkManagerConfigurationTests.cs index ea4a4b155b..29ca4fead8 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/NetworkManagerConfigurationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/NetworkManagerConfigurationTests.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using NUnit.Framework; using Unity.Netcode.Editor; +using Unity.Netcode.Logging; using Unity.Netcode.Transports.UTP; using UnityEditor.SceneManagement; using UnityEngine; @@ -145,7 +146,8 @@ public void WhenNetworkConfigContainsOldPrefabList_TheyMigrateProperlyToTheNewLi new NetworkPrefab { Prefab = overriddenPrefab.gameObject, Override = NetworkPrefabOverride.Prefab, OverridingTargetPrefab = overridingTargetPrefab.gameObject, SourcePrefabToOverride = sourcePrefabToOverride.gameObject, SourceHashToOverride = 123456 } }; - networkConfig.InitializePrefabs(); + var log = new ContextualLogger(); + networkConfig.InitializePrefabs(log); Assert.IsNull(networkConfig.OldPrefabList); Assert.IsNotNull(networkConfig.Prefabs); @@ -303,7 +305,8 @@ public void WhenThereAreUninitializedElementsInPrefabsList_NoErrors() Assert.That(networkConfig.Prefabs.NetworkPrefabsLists.Count, Is.EqualTo(2), "Failed test setup: Both the null element and the list containing a null Prefab should be listed"); - networkConfig.InitializePrefabs(); + var log = new ContextualLogger(); + networkConfig.InitializePrefabs(log); Assert.That(networkConfig.Prefabs.NetworkPrefabsLists.Count, Is.EqualTo(1), "null element should have been removed"); Assert.That(networkConfig.Prefabs.Prefabs.Count, Is.EqualTo(0), "Invalid prefab was registered");