#if !FUSION_DEV #region Assets/Photon/Fusion/Runtime/AssemblyAttributes/FusionAssemblyAttributes.Common.cs // merged AssemblyAttributes #region RegisterResourcesLoader.cs // register a default loader; it will attempt to load the asset from their default paths if they happen to be Resources [assembly:Fusion.FusionGlobalScriptableObjectResource(typeof(Fusion.FusionGlobalScriptableObject), Order = 2000, AllowFallback = true)] #endregion #endregion #region Assets/Photon/Fusion/Runtime/FusionAssetSource.Common.cs // merged AssetSource #region NetworkAssetSourceAddressable.cs #if (FUSION_ADDRESSABLES || FUSION_ENABLE_ADDRESSABLES) && !FUSION_DISABLE_ADDRESSABLES namespace Fusion { using System; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using Object = UnityEngine.Object; [Serializable] public partial class NetworkAssetSourceAddressable where T : UnityEngine.Object { public AssetReference Address; [NonSerialized] private int _acquireCount; public void Acquire(bool synchronous) { if (_acquireCount == 0) { LoadInternal(synchronous); } _acquireCount++; } public void Release() { if (_acquireCount <= 0) { throw new Exception("Asset is not loaded"); } if (--_acquireCount == 0) { UnloadInternal(); } } public bool IsCompleted => Address.IsDone; public T WaitForResult() { Debug.Assert(Address.IsValid()); var op = Address.OperationHandle; if (!op.IsDone) { try { op.WaitForCompletion(); } catch (Exception e) when (!Application.isPlaying && typeof(Exception) == e.GetType()) { Debug.LogError($"An exception was thrown when loading asset: {Address}; since this method " + $"was called from the editor, it may be due to the fact that Addressables don't have edit-time load support. Please use EditorInstance instead."); throw; } } if (op.OperationException != null) { throw new InvalidOperationException($"Failed to load asset: {Address}", op.OperationException); } Debug.AssertFormat(op.Result != null, "op.Result != null"); return ValidateResult(op.Result); } private void LoadInternal(bool synchronous) { Debug.Assert(!Address.IsValid()); var op = Address.LoadAssetAsync(); if (!op.IsValid()) { throw new Exception($"Failed to load asset: {Address}"); } if (op.Status == AsyncOperationStatus.Failed) { throw new Exception($"Failed to load asset: {Address}", op.OperationException); } if (synchronous) { op.WaitForCompletion(); } } private void UnloadInternal() { if (Address.IsValid()) { Address.ReleaseAsset(); } } private T ValidateResult(object result) { if (result == null) { throw new InvalidOperationException($"Failed to load asset: {Address}; asset is null"); } if (typeof(T).IsSubclassOf(typeof(Component))) { if (result is GameObject gameObject == false) { throw new InvalidOperationException($"Failed to load asset: {Address}; asset is not a GameObject, but a {result.GetType()}"); } var component = ((GameObject)result).GetComponent(); if (!component) { throw new InvalidOperationException($"Failed to load asset: {Address}; asset does not contain component {typeof(T)}"); } return component; } if (result is T asset) { return asset; } throw new InvalidOperationException($"Failed to load asset: {Address}; asset is not of type {typeof(T)}, but {result.GetType()}"); } public string Description => "Address: " + Address.RuntimeKey; #if UNITY_EDITOR public T EditorInstance { get { var editorAsset = Address.editorAsset; if (string.IsNullOrEmpty(Address.SubObjectName)) { return ValidateResult(editorAsset); } else { var assetPath = AssetDatabase.GUIDToAssetPath(Address.AssetGUID); var assets = AssetDatabase.LoadAllAssetsAtPath(assetPath); foreach (var asset in assets) { if (asset.name == Address.SubObjectName) { return ValidateResult(asset); } } return null; } } } #endif } } #endif #endregion #region NetworkAssetSourceResource.cs namespace Fusion { using System; using System.Runtime.ExceptionServices; using UnityEngine; using Object = UnityEngine.Object; using UnityResources = UnityEngine.Resources; [Serializable] public partial class NetworkAssetSourceResource where T : UnityEngine.Object { [UnityResourcePath(typeof(Object))] public string ResourcePath; public string SubObjectName; [NonSerialized] private object _state; [NonSerialized] private int _acquireCount; public void Acquire(bool synchronous) { if (_acquireCount == 0) { LoadInternal(synchronous); } _acquireCount++; } public void Release() { if (_acquireCount <= 0) { throw new Exception("Asset is not loaded"); } if (--_acquireCount == 0) { UnloadInternal(); } } public bool IsCompleted { get { if (_state == null) { // hasn't started return false; } if (_state is ResourceRequest asyncOp && !asyncOp.isDone) { // still loading, wait return false; } return true; } } public T WaitForResult() { Debug.Assert(_state != null); if (_state is ResourceRequest asyncOp) { if (asyncOp.isDone) { FinishAsyncOp(asyncOp); } else { // just load synchronously, then pass through _state = null; LoadInternal(synchronous: true); } } if (_state == null) { throw new InvalidOperationException($"Failed to load asset {typeof(T)}: {ResourcePath}[{SubObjectName}]. Asset is null."); } if (_state is T asset) { return asset; } if (_state is ExceptionDispatchInfo exception) { exception.Throw(); throw new NotSupportedException(); } throw new InvalidOperationException($"Failed to load asset {typeof(T)}: {ResourcePath}, SubObjectName: {SubObjectName}"); } private void FinishAsyncOp(ResourceRequest asyncOp) { try { var asset = string.IsNullOrEmpty(SubObjectName) ? asyncOp.asset : LoadNamedResource(ResourcePath, SubObjectName); if (asset) { _state = asset; } else { throw new InvalidOperationException($"Missing Resource: {ResourcePath}, SubObjectName: {SubObjectName}"); } } catch (Exception ex) { _state = ExceptionDispatchInfo.Capture(ex); } } private static T LoadNamedResource(string resoucePath, string subObjectName) { var assets = UnityResources.LoadAll(resoucePath); for (var i = 0; i < assets.Length; ++i) { var asset = assets[i]; if (string.Equals(asset.name, subObjectName, StringComparison.Ordinal)) { return asset; } } return null; } private void LoadInternal(bool synchronous) { Debug.Assert(_state == null); try { if (synchronous) { _state = string.IsNullOrEmpty(SubObjectName) ? UnityResources.Load(ResourcePath) : LoadNamedResource(ResourcePath, SubObjectName); } else { _state = UnityResources.LoadAsync(ResourcePath); } if (_state == null) { _state = new InvalidOperationException($"Missing Resource: {ResourcePath}, SubObjectName: {SubObjectName}"); } } catch (Exception ex) { _state = ExceptionDispatchInfo.Capture(ex); } } private void UnloadInternal() { if (_state is ResourceRequest asyncOp) { asyncOp.completed += op => { // unload stuff }; } else if (_state is Object) { // unload stuff } _state = null; } public string Description => $"Resource: {ResourcePath}{(!string.IsNullOrEmpty(SubObjectName) ? $"[{SubObjectName}]" : "")}"; #if UNITY_EDITOR public T EditorInstance => string.IsNullOrEmpty(SubObjectName) ? UnityResources.Load(ResourcePath) : LoadNamedResource(ResourcePath, SubObjectName); #endif } } #endregion #region NetworkAssetSourceStatic.cs namespace Fusion { using System; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Serialization; using Object = UnityEngine.Object; [Serializable] public partial class NetworkAssetSourceStatic where T : UnityEngine.Object { [FormerlySerializedAs("Prefab")] public T Object; [Obsolete("Use Asset instead")] public T Prefab { get => Object; set => Object = value; } public bool IsCompleted => true; public void Acquire(bool synchronous) { // do nothing } public void Release() { // do nothing } public T WaitForResult() { if (Object == null) { throw new InvalidOperationException("Missing static reference"); } return Object; } public string Description { get { if (Object) { #if UNITY_EDITOR if (UnityEditor.AssetDatabase.TryGetGUIDAndLocalFileIdentifier(Object, out var guid, out long fileID)) { return $"Static: {guid}, fileID: {fileID}"; } #endif return "Static: " + Object; } else { return "Static: (null)"; } } } #if UNITY_EDITOR public T EditorInstance => Object; #endif } } #endregion #region NetworkAssetSourceStaticLazy.cs namespace Fusion { using System; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Serialization; using Object = UnityEngine.Object; [Serializable] public partial class NetworkAssetSourceStaticLazy where T : UnityEngine.Object { [FormerlySerializedAs("Prefab")] public LazyLoadReference Object; [Obsolete("Use Object instead")] public LazyLoadReference Prefab { get => Object; set => Object = value; } public bool IsCompleted => true; public void Acquire(bool synchronous) { // do nothing } public void Release() { // do nothing } public T WaitForResult() { if (Object.asset == null) { throw new InvalidOperationException("Missing static reference"); } return Object.asset; } public string Description { get { if (Object.isBroken) { return "Static: (broken)"; } else if (Object.isSet) { #if UNITY_EDITOR if (UnityEditor.AssetDatabase.TryGetGUIDAndLocalFileIdentifier(Object.instanceID, out var guid, out long fileID)) { return $"Static: {guid}, fileID: {fileID}"; } #endif return "Static: " + Object.asset; } else { return "Static: (null)"; } } } #if UNITY_EDITOR public T EditorInstance => Object.asset; #endif } } #endregion #region FusionGlobalScriptableObjectAddressAttribute.cs namespace Fusion { using System; using UnityEngine.Scripting; #if (FUSION_ADDRESSABLES || FUSION_ENABLE_ADDRESSABLES) && !FUSION_DISABLE_ADDRESSABLES using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; #endif [Preserve] public class FusionGlobalScriptableObjectAddressAttribute : FusionGlobalScriptableObjectSourceAttribute { public FusionGlobalScriptableObjectAddressAttribute(Type objectType, string address) : base(objectType) { Address = address; } public string Address { get; } public override FusionGlobalScriptableObjectLoadResult Load(Type type) { #if (FUSION_ADDRESSABLES || FUSION_ENABLE_ADDRESSABLES) && !FUSION_DISABLE_ADDRESSABLES Assert.Check(!string.IsNullOrEmpty(Address)); var op = Addressables.LoadAssetAsync(Address); var instance = op.WaitForCompletion(); if (op.Status == AsyncOperationStatus.Succeeded) { Assert.Check(instance); return new (instance, x => Addressables.Release(op)); } Log.Trace($"Failed to load addressable at address {Address} for type {type.FullName}: {op.OperationException}"); return default; #else Log.Trace($"Addressables are not enabled. Unable to load addressable for {type.FullName}"); return default; #endif } } } #endregion #region FusionGlobalScriptableObjectResourceAttribute.cs namespace Fusion { using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using UnityEngine; using UnityEngine.Scripting; using Object = UnityEngine.Object; [Preserve] public class FusionGlobalScriptableObjectResourceAttribute : FusionGlobalScriptableObjectSourceAttribute { public FusionGlobalScriptableObjectResourceAttribute(Type objectType, string resourcePath = "") : base(objectType) { ResourcePath = resourcePath; } public string ResourcePath { get; } public bool InstantiateIfLoadedInEditor { get; set; } = true; public override FusionGlobalScriptableObjectLoadResult Load(Type type) { var attribute = type.GetCustomAttribute(); Assert.Check(attribute != null); string resourcePath; if (string.IsNullOrEmpty(ResourcePath)) { string defaultAssetPath = attribute.DefaultPath; var indexOfResources = defaultAssetPath.LastIndexOf("/Resources/", StringComparison.OrdinalIgnoreCase); if (indexOfResources < 0) { Log.Trace($"The default path {defaultAssetPath} does not contain a /Resources/ folder. Unable to load resource for {type.FullName}."); return default; } // try to load from resources, maybe? resourcePath = defaultAssetPath.Substring(indexOfResources + "/Resources/".Length); // drop the extension if (Path.HasExtension(resourcePath)) { resourcePath = resourcePath.Substring(0, resourcePath.LastIndexOf('.')); } } else { resourcePath = ResourcePath; } var instance = UnityEngine.Resources.Load(resourcePath, type); if (!instance) { Log.Trace($"Unable to load resource at path {resourcePath} for type {type.FullName}"); return default; } if (InstantiateIfLoadedInEditor && Application.isEditor) { var clone = Object.Instantiate(instance); return new((FusionGlobalScriptableObject)clone, x => Object.Destroy(clone)); } else { return new((FusionGlobalScriptableObject)instance, x => UnityEngine.Resources.UnloadAsset(instance)); } } } } #endregion #endregion #region Assets/Photon/Fusion/Runtime/FusionBurstIntegration.cs // deleted #endregion #region Assets/Photon/Fusion/Runtime/FusionCoroutine.cs  namespace Fusion { using UnityEngine; using System; using System.Collections; using System.Runtime.ExceptionServices; public sealed class FusionCoroutine : ICoroutine, IDisposable { private readonly IEnumerator _inner; private Action _completed; private float _progress; private Action _activateAsync; public FusionCoroutine(IEnumerator inner) { _inner = inner ?? throw new ArgumentNullException(nameof(inner)); } public event Action Completed { add { _completed += value; if (IsDone) { value(this); } } remove => _completed -= value; } public bool IsDone { get; private set; } public ExceptionDispatchInfo Error { get; private set; } bool IEnumerator.MoveNext() { try { if (_inner.MoveNext()) { return true; } else { IsDone = true; _completed?.Invoke(this); return false; } } catch (Exception e) { IsDone = true; Error = ExceptionDispatchInfo.Capture(e); _completed?.Invoke(this); return false; } } void IEnumerator.Reset() { _inner.Reset(); IsDone = false; Error = null; } object IEnumerator.Current => _inner.Current; public void Dispose() { if (_inner is IDisposable disposable) { disposable.Dispose(); } } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionProfiler.cs namespace Fusion { #if FUSION_PROFILER_INTEGRATION using Unity.Profiling; using UnityEngine; public static class FusionProfiler { [RuntimeInitializeOnLoadMethod] static void Init() { Fusion.EngineProfiler.InterpolationOffsetCallback = f => InterpolationOffset.Sample(f); Fusion.EngineProfiler.InterpolationTimeScaleCallback = f => InterpolationTimeScale.Sample(f); Fusion.EngineProfiler.InterpolationMultiplierCallback = f => InterpolationMultiplier.Sample(f); Fusion.EngineProfiler.InterpolationUncertaintyCallback = f => InterpolationUncertainty.Sample(f); Fusion.EngineProfiler.ResimulationsCallback = i => Resimulations.Sample(i); Fusion.EngineProfiler.WorldSnapshotSizeCallback = i => WorldSnapshotSize.Sample(i); Fusion.EngineProfiler.RoundTripTimeCallback = f => RoundTripTime.Sample(f); Fusion.EngineProfiler.InputSizeCallback = i => InputSize.Sample(i); Fusion.EngineProfiler.InputQueueCallback = i => InputQueue.Sample(i); Fusion.EngineProfiler.RpcInCallback = i => RpcIn.Value += i; Fusion.EngineProfiler.RpcOutCallback = i => RpcOut.Value += i; Fusion.EngineProfiler.SimualtionTimeScaleCallback = f => SimulationTimeScale.Sample(f); Fusion.EngineProfiler.InputOffsetCallback = f => InputOffset.Sample(f); Fusion.EngineProfiler.InputOffsetDeviationCallback = f => InputOffsetDeviation.Sample(f); Fusion.EngineProfiler.InputRecvDeltaCallback = f => InputRecvDelta.Sample(f); Fusion.EngineProfiler.InputRecvDeltaDeviationCallback = f => InputRecvDeltaDeviation.Sample(f); } public static readonly ProfilerCategory Category = ProfilerCategory.Scripts; public static readonly ProfilerCounter InterpolationOffset = new ProfilerCounter(Category, "Interp Offset", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InterpolationTimeScale = new ProfilerCounter(Category, "Interp Time Scale", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InterpolationMultiplier = new ProfilerCounter(Category, "Interp Multiplier", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InterpolationUncertainty = new ProfilerCounter(Category, "Interp Uncertainty", ProfilerMarkerDataUnit.Undefined); public static readonly ProfilerCounter InputSize = new ProfilerCounter(Category, "Client Input Size", ProfilerMarkerDataUnit.Bytes); public static readonly ProfilerCounter InputQueue = new ProfilerCounter(Category, "Client Input Queue", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter WorldSnapshotSize = new ProfilerCounter(Category, "Client Snapshot Size", ProfilerMarkerDataUnit.Bytes); public static readonly ProfilerCounter Resimulations = new ProfilerCounter(Category, "Client Resims", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter RoundTripTime = new ProfilerCounter(Category, "Client RTT", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounterValue RpcIn = new ProfilerCounterValue(Category, "RPCs In", ProfilerMarkerDataUnit.Count, ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); public static readonly ProfilerCounterValue RpcOut = new ProfilerCounterValue(Category, "RPCs Out", ProfilerMarkerDataUnit.Count, ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); public static readonly ProfilerCounter SimulationTimeScale = new ProfilerCounter(Category, "Simulation Time Scale", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InputOffset = new ProfilerCounter(Category, "Input Offset", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InputOffsetDeviation = new ProfilerCounter(Category, "Input Offset Dev", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InputRecvDelta = new ProfilerCounter(Category, "Input Recv Delta", ProfilerMarkerDataUnit.Count); public static readonly ProfilerCounter InputRecvDeltaDeviation = new ProfilerCounter(Category, "Input Recv Delta Dev", ProfilerMarkerDataUnit.Count); } #endif } #endregion #region Assets/Photon/Fusion/Runtime/FusionRuntimeCheck.cs namespace Fusion { using UnityEngine; static class FusionRuntimeCheck { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void RuntimeCheck() { RuntimeUnityFlagsSetup.Check_ENABLE_IL2CPP(); RuntimeUnityFlagsSetup.Check_ENABLE_MONO(); RuntimeUnityFlagsSetup.Check_UNITY_EDITOR(); RuntimeUnityFlagsSetup.Check_UNITY_GAMECORE(); RuntimeUnityFlagsSetup.Check_UNITY_SWITCH(); RuntimeUnityFlagsSetup.Check_UNITY_WEBGL(); RuntimeUnityFlagsSetup.Check_UNITY_XBOXONE(); RuntimeUnityFlagsSetup.Check_NETFX_CORE(); RuntimeUnityFlagsSetup.Check_NET_4_6(); RuntimeUnityFlagsSetup.Check_NET_STANDARD_2_0(); RuntimeUnityFlagsSetup.Check_UNITY_2019_4_OR_NEWER(); } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStats.Controls.cs namespace Fusion { using StatsInternal; using UnityEditor; public partial class FusionStats { const int SCREEN_SCALE_W = 1080; const int SCREEN_SCALE_H = 1080; const float TEXT_MARGIN = 0.25f; const float TITLE_HEIGHT = 20f; const int MARGIN = FusionStatsUtilities.MARGIN; const int PAD = FusionStatsUtilities.PAD; const string PLAY_TEXT = "PLAY"; const string PAUS_TEXT = "PAUSE"; const string SHOW_TEXT = "SHOW"; const string HIDE_TEXT = "HIDE"; const string CLER_TEXT = "CLEAR"; const string CNVS_TEXT = "CANVAS"; const string CLSE_TEXT = "CLOSE"; const string PLAY_ICON = "\u25ba"; const string PAUS_ICON = "\u05f0"; const string HIDE_ICON = "\u25bc"; const string SHOW_ICON = "\u25b2"; const string CLER_ICON = "\u1d13"; const string CNVS_ICON = "\ufb26"; //"\u2261"; const string CLSE_ICON = "x"; void InitializeControls() { // Listener connections are not retained with serialization and always need to be connected at startup. // Remove listeners in case this is a copy of a runtime generated graph. _togglButton?.onClick.RemoveListener(Toggle); _canvsButton?.onClick.RemoveListener(ToggleCanvasType); _clearButton?.onClick.RemoveListener(Clear); _pauseButton?.onClick.RemoveListener(Pause); _closeButton?.onClick.RemoveListener(Close); _titleButton?.onClick.RemoveListener(PingSelectFusionStats); _objctButton?.onClick.RemoveListener(PingSelectObject); _togglButton?.onClick.AddListener(Toggle); _canvsButton?.onClick.AddListener(ToggleCanvasType); _clearButton?.onClick.AddListener(Clear); _pauseButton?.onClick.AddListener(Pause); _closeButton?.onClick.AddListener(Close); _titleButton?.onClick.AddListener(PingSelectFusionStats); _objctButton?.onClick.AddListener(PingSelectObject); } void Pause() { if (_runner && _runner.IsRunning) { _paused = !_paused; var icon = _paused ? PLAY_ICON : PAUS_ICON; var label = _paused ? PLAY_TEXT : PAUS_TEXT; _pauseIcon.text = icon; _pauseLabel.text = label; // Pause for all SimStats tied to this runner if all related FusionStats are paused. if (_statsForRunnerLookup.TryGetValue(_runner, out var stats)) { // bool statsAreBeingUsed = false; foreach (var stat in stats) { if (stat._paused == false) { // statsAreBeingUsed = true; break; } } // Pause in 2.0 should probably be removed // _runner.Simulation.Stats.Pause(statsAreBeingUsed == false); } } } void Toggle() { _hidden = !_hidden; _togglIcon.text = _hidden ? SHOW_ICON : HIDE_ICON; _togglLabel.text = _hidden ? SHOW_TEXT : HIDE_TEXT; _statsPanelRT.gameObject.SetActive(!_hidden); for (int i = 0; i < _simGraphs.Length; ++i) { var graph = _simGraphs[i]; if (graph) { _simGraphs[i].gameObject.SetActive(!_hidden && ((long)1 << i & _includedSimStats.Mask) != 0); } } for (int i = 0; i < _objGraphs.Length; ++i) { var graph = _objGraphs[i]; if (graph) { _objGraphs[i].gameObject.SetActive(!_hidden && ((long)1 << i & _includedObjStats.Mask) != 0); } } for (int i = 0; i < _netGraphs.Length; ++i) { var graph = _netGraphs[i]; if (graph) { _netGraphs[i].gameObject.SetActive(!_hidden && ((long)1 << i & _includedNetStats.Mask) != 0); } } } void Clear() { if (_runner && _runner.IsRunning) { // Clear in 2.0 likely needs to be removed // _runner.Simulation.Stats.Clear(); } for (int i = 0; i < _simGraphs.Length; ++i) { var graph = _simGraphs[i]; if (graph) { _simGraphs[i].Clear(); } } for (int i = 0; i < _objGraphs.Length; ++i) { var graph = _objGraphs[i]; if (graph) { _objGraphs[i].Clear(); } } for (int i = 0; i < _netGraphs.Length; ++i) { var graph = _netGraphs[i]; if (graph) { _netGraphs[i].Clear(); } } } void ToggleCanvasType() { #if UNITY_EDITOR UnityEditor.EditorGUIUtility.PingObject(gameObject); if (Selection.activeGameObject == null) { Selection.activeGameObject = gameObject; } #endif _canvasType = (_canvasType == StatCanvasTypes.GameObject) ? StatCanvasTypes.Overlay : StatCanvasTypes.GameObject; //_canvas.enabled = false; _layoutDirty = 3; CalculateLayout(); } void Close() { Destroy(this.gameObject); } void PingSelectObject() { #if UNITY_EDITOR var obj = Object; if (obj) { EditorGUIUtility.PingObject(Object.gameObject); Selection.activeGameObject = Object.gameObject; } #endif } void PingSelectFusionStats() { #if UNITY_EDITOR EditorGUIUtility.PingObject(gameObject); Selection.activeGameObject = gameObject; #endif } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStats.Create.cs namespace Fusion { using System; using System.Collections.Generic; using StatsInternal; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public partial class FusionStats { #if UNITY_EDITOR [MenuItem("Tools/Fusion/Scene/Add Fusion Stats", false, 1000)] [MenuItem("GameObject/Fusion/Scene/Add Fusion Stats", false, 0)] public static void AddFusionStatsToScene() { var selected = Selection.activeGameObject; if (selected && PrefabUtility.IsPartOfPrefabAsset(selected)) { Debug.LogWarning("Open prefabs before running 'Add Fusion Stats' on them."); return; } var fs = new GameObject("FusionStats"); if (selected) { fs.transform.SetParent(Selection.activeGameObject.transform); } fs.transform.localPosition = default; fs.transform.localRotation = default; fs.transform.localScale = Vector3.one; fs.AddComponent(); fs.AddComponent(); EditorGUIUtility.PingObject(fs.gameObject); Selection.activeGameObject = fs.gameObject; } #endif [SerializeField][HideInInspector] FusionStatsGraph[] _simGraphs; [SerializeField][HideInInspector] FusionStatsGraph[] _objGraphs; [SerializeField][HideInInspector] FusionStatsGraph[] _netGraphs; [SerializeField][HideInInspector] FusionStatsObjectIds _objIds; [NonSerialized] List _foundViews; [NonSerialized] List _foundGraphs; [SerializeField][HideInInspector] UnityEngine.UI.Text _titleText; [SerializeField][HideInInspector] UnityEngine.UI.Text _clearIcon; [SerializeField][HideInInspector] UnityEngine.UI.Text _pauseIcon; [SerializeField][HideInInspector] UnityEngine.UI.Text _togglIcon; [SerializeField][HideInInspector] UnityEngine.UI.Text _closeIcon; [SerializeField][HideInInspector] UnityEngine.UI.Text _canvsIcon; [SerializeField][HideInInspector] UnityEngine.UI.Text _clearLabel; [SerializeField][HideInInspector] UnityEngine.UI.Text _pauseLabel; [SerializeField][HideInInspector] UnityEngine.UI.Text _togglLabel; [SerializeField][HideInInspector] UnityEngine.UI.Text _closeLabel; [SerializeField][HideInInspector] UnityEngine.UI.Text _canvsLabel; [SerializeField][HideInInspector] UnityEngine.UI.Text _objectNameText; [SerializeField][HideInInspector] UnityEngine.UI.GridLayoutGroup _graphGridLayoutGroup; [SerializeField][HideInInspector] Canvas _canvas; [SerializeField][HideInInspector] RectTransform _canvasRT; [SerializeField][HideInInspector] RectTransform _rootPanelRT; [SerializeField][HideInInspector] RectTransform _headerRT; [SerializeField][HideInInspector] RectTransform _statsPanelRT; [SerializeField][HideInInspector] RectTransform _graphsLayoutRT; [SerializeField][HideInInspector] RectTransform _titleRT; [SerializeField][HideInInspector] RectTransform _buttonsRT; [SerializeField][HideInInspector] RectTransform _objectTitlePanelRT; [SerializeField][HideInInspector] RectTransform _objectIdsGroupRT; [SerializeField][HideInInspector] RectTransform _objectMetersPanelRT; [SerializeField][HideInInspector] UnityEngine.UI.Button _titleButton; [SerializeField][HideInInspector] UnityEngine.UI.Button _objctButton; [SerializeField][HideInInspector] UnityEngine.UI.Button _clearButton; [SerializeField][HideInInspector] UnityEngine.UI.Button _togglButton; [SerializeField][HideInInspector] UnityEngine.UI.Button _pauseButton; [SerializeField][HideInInspector] UnityEngine.UI.Button _closeButton; [SerializeField][HideInInspector] UnityEngine.UI.Button _canvsButton; /// /// Creates a new GameObject with a component, attaches it to any supplied parent, and generates Canvas/Graphs. /// /// /// Generated FusionStats component and GameObject will be added as a child of this transform. /// Uses a predefined position. /// The network stats to be enabled. If left null, default statistics will be used. /// The simulation stats to be enabled. If left null, default statistics will be used. public static FusionStats Create(Transform parent = null, NetworkRunner runner = null, DefaultLayouts? screenLayout = null, DefaultLayouts? objectLayout = null/*, Stats.NetStatFlags? netStatsMask = null, Stats.SimStatFlags? simStatsMask = null*/) { var go = new GameObject($"{nameof(FusionStats)} {(runner ? runner.name : "null")}"); FusionStats stats; if (parent) { go.transform.SetParent(parent); } stats = go.AddComponent(); stats.ResetLayout(null, null, screenLayout); stats.SetRunner(runner); if (runner != null) { stats.AutoDestroy = true; } return stats; } bool _graphsAreMissing => _canvasRT == null; [EditorButton(EditorButtonVisibility.EditMode)] [DrawIf(nameof(_graphsAreMissing), Hide = true)] void GenerateGraphs() { var rootRectTr = gameObject.GetComponent(); _canvasRT = rootRectTr.CreateRectTransform("Stats Canvas"); _canvas = _canvasRT.gameObject.AddComponent(); _canvas.pixelPerfect = true; _canvas.renderMode = RenderMode.ScreenSpaceOverlay; // If the runner has already started, the root FusionStats has been added to the VisNodes registration for the runner, // But any generated children GOs here will not. Add the generated components to the visibility system. if (Runner && Runner.IsRunning) { Runner.AddVisibilityNodes(_canvasRT.gameObject); } var scaler = _canvasRT.gameObject.AddComponent(); scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(SCREEN_SCALE_W, SCREEN_SCALE_H); scaler.matchWidthOrHeight = .4f; _canvasRT.gameObject.AddComponent(); _rootPanelRT = _canvasRT .CreateRectTransform("Root Panel"); _headerRT = _rootPanelRT .CreateRectTransform("Header Panel") .AddCircleSprite(PanelColor); _titleRT = _headerRT .CreateRectTransform("Runner Title") .SetAnchors(0.0f, 1.0f, 0.75f, 1.0f) .SetOffsets(MARGIN, -MARGIN, 0.0f, -MARGIN); _titleButton = _titleRT.gameObject.AddComponent(); _titleText = _titleRT.AddText(_runner ? _runner.name : "Disconnected", TextAnchor.UpperCenter, _fontColor, LabelFont); _titleText.raycastTarget = true; // Buttons _buttonsRT = _headerRT .CreateRectTransform("Buttons") .SetAnchors(0.0f, 1.0f, 0.0f, 0.75f) .SetOffsets(MARGIN, -MARGIN, MARGIN, 0); var buttonsGrid = _buttonsRT.gameObject.AddComponent(); buttonsGrid.childControlHeight = true; buttonsGrid.childControlWidth = true; buttonsGrid.spacing = MARGIN; _buttonsRT.MakeButton(ref _togglButton, HIDE_ICON, HIDE_TEXT, LabelFont, out _togglIcon, out _togglLabel, Toggle); _buttonsRT.MakeButton(ref _canvsButton, CNVS_ICON, CNVS_TEXT, LabelFont, out _canvsIcon, out _canvsLabel, ToggleCanvasType); _buttonsRT.MakeButton(ref _pauseButton, PAUS_ICON, PAUS_TEXT, LabelFont, out _pauseIcon, out _pauseLabel, Pause); _buttonsRT.MakeButton(ref _clearButton, CLER_ICON, CLER_TEXT, LabelFont, out _clearIcon, out _clearLabel, Clear); _buttonsRT.MakeButton(ref _closeButton, CLSE_ICON, CLSE_TEXT, LabelFont, out _closeIcon, out _closeLabel, Close); // Minor tweak to foldout arrow icon, since its too tall. _togglIcon.rectTransform.anchorMax = new Vector2(1, 0.85f); // Stats stack _statsPanelRT = _rootPanelRT .CreateRectTransform("Stats Panel") .AddCircleSprite(PanelColor); // Object Name, IDs and Meters _objectTitlePanelRT = _statsPanelRT .CreateRectTransform("Object Name Panel") .ExpandTopAnchor(MARGIN) .AddCircleSprite(_objDataBackColor); _objctButton = _objectTitlePanelRT.gameObject.AddComponent(); var objectTitleRT = _objectTitlePanelRT .CreateRectTransform("Object Name") .SetAnchors(0.0f, 1.0f, 0.15f, 0.85f) .SetOffsets(PAD, -PAD, 0, 0); _objectNameText = objectTitleRT.AddText("Object Name", TextAnchor.MiddleCenter, _fontColor, LabelFont); _objectNameText.alignByGeometry = false; _objectNameText.raycastTarget = false; _objIds = FusionStatsObjectIds.Create(_statsPanelRT, this, out _objectIdsGroupRT); _objectMetersPanelRT = _statsPanelRT .CreateRectTransform("Object Meters Layout") .ExpandTopAnchor(MARGIN) .AddVerticalLayoutGroup(MARGIN); // These are placeholders to connect stats 2.0 to old 1.0 enums const int INDEX_OF_OBJ_BANDWIDTH = 0; const int INDEX_OF_OBJ_RPC_COUNT = 1; FusionStatsMeterBar.Create(_objectMetersPanelRT, this, StatSourceTypes.NetworkObject, INDEX_OF_OBJ_BANDWIDTH, 15, 30); FusionStatsMeterBar.Create(_objectMetersPanelRT, this, StatSourceTypes.NetworkObject, INDEX_OF_OBJ_RPC_COUNT, 3, 6); // Graphs _graphsLayoutRT = _statsPanelRT .CreateRectTransform("Graphs Layout") .ExpandAnchor() .SetOffsets(MARGIN, 0, 0, 0); //.AddGridlLayoutGroup(MRGN); _graphGridLayoutGroup = _graphsLayoutRT.AddGridlLayoutGroup(MARGIN); int objTypeCount = StatSourceTypes.NetworkObject.Lookup().LongNames.Length; _objGraphs = new FusionStatsGraph[objTypeCount]; for (int i = 0; i < objTypeCount; ++i) { if (InitializeAllGraphs == false) { var statFlag = (long)1 << i; if ((statFlag & _includedObjStats.Mask) == 0) { continue; } } CreateGraph(StatSourceTypes.NetworkObject, i, _graphsLayoutRT); } int netConnTypeCount = StatSourceTypes.NetConnection.Lookup().LongNames.Length; _netGraphs = new FusionStatsGraph[netConnTypeCount]; for (int i = 0; i < netConnTypeCount; ++i) { if (InitializeAllGraphs == false) { var statFlag = (long)1 << i; if ((statFlag & _includedNetStats.Mask) == 0) { continue; } } CreateGraph(StatSourceTypes.NetConnection, i, _graphsLayoutRT); } int simTypeCount = StatSourceTypes.Simulation.Lookup().LongNames.Length; _simGraphs = new FusionStatsGraph[simTypeCount]; for (int i = 0; i < simTypeCount; ++i) { if (InitializeAllGraphs == false) { var statFlag = ((long)1 << i); if ((statFlag & _includedSimStats.Mask) == 0) { continue; } } CreateGraph(StatSourceTypes.Simulation, i, _graphsLayoutRT); } _activeDirty = true; _layoutDirty = 2; } public FusionStatsGraph CreateGraph(StatSourceTypes type, int statId, RectTransform parentRT) { var fg = FusionStatsGraph.Create(this, type, statId, parentRT); if (type == StatSourceTypes.Simulation) { _simGraphs[statId] = fg; if ((_includedSimStats.Mask & ((long)1 << statId)) == 0) { fg.gameObject.SetActive(false); } } else if (type == StatSourceTypes.NetworkObject) { _objGraphs[statId] = fg; if ((_includedObjStats.Mask & ((long)1 << statId)) == 0) { fg.gameObject.SetActive(false); } } else { _netGraphs[statId] = fg; if ((_includedNetStats.Mask & ((long)1 << statId)) == 0) { fg.gameObject.SetActive(false); } } return fg; } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStats.Layout.cs namespace Fusion { using System; using System.Collections.Generic; using StatsInternal; using UnityEngine; public partial class FusionStats { void UpdateTitle() { var runnername = _runner ? _runner.name : "Disconnected"; if (_titleText) { _titleText.text = runnername; } } void DirtyLayout(int minimumRefreshes = 1) { if (_layoutDirty < minimumRefreshes) { _layoutDirty = minimumRefreshes; } } float _lastLayoutUpdate; void CalculateLayout() { if (_rootPanelRT == null || _graphsLayoutRT == null) { return; } if (_foundGraphs == null) { _foundGraphs = new List(_graphsLayoutRT.GetComponentsInChildren(false)); } else { GetComponentsInChildren(false, _foundGraphs); } // Don't count multiple executions of CalculateLayout in the same Update as reducing the dirty count. // _layoutDirty can be set to values greater than 1 to force a recalculate for several consecutive Updates. var time = Time.time; if (_lastLayoutUpdate < time) { _layoutDirty--; _lastLayoutUpdate = time; } #if UNITY_EDITOR if (Application.isPlaying == false && _layoutDirty > 0) { UnityEditor.EditorApplication.delayCall -= CalculateLayout; UnityEditor.EditorApplication.delayCall += CalculateLayout; } #endif if (_layoutDirty <= 0 && _canvas.enabled == false) { //_canvas.enabled = true; } if (_rootPanelRT) { var maxHeaderHeight = Math.Min(_maxHeaderHeight, _rootPanelRT.rect.width / 4); if (_canvasType == StatCanvasTypes.GameObject) { _canvas.renderMode = RenderMode.WorldSpace; var scale = CanvasScale / SCREEN_SCALE_H; // (1f / SCREEN_SCALE_H) * Scale; _canvasRT.localScale = new Vector3(scale, scale, scale); _canvasRT.sizeDelta = new Vector2(1024, 1024); _canvasRT.localPosition = new Vector3(0, 0, CanvasDistance); // TODO: Cache this if (_canvasRT.GetComponent() == false) { _canvasRT.localRotation = default; } } else { _canvas.renderMode = RenderMode.ScreenSpaceOverlay; } _objectTitlePanelRT.gameObject.SetActive(_enableObjectStats); _objectIdsGroupRT.gameObject.SetActive(_enableObjectStats); _objectMetersPanelRT.gameObject.SetActive(_enableObjectStats); Vector2 icoMinAnchor; if (_showButtonLabels) { icoMinAnchor = new Vector2(0.0f, FusionStatsUtilities.BTTN_LBL_NORM_HGHT * .5f); } else { icoMinAnchor = new Vector2(0.0f, 0.0f); } _togglIcon.rectTransform.anchorMin = icoMinAnchor + new Vector2(0, .15f); _canvsIcon.rectTransform.anchorMin = icoMinAnchor; _clearIcon.rectTransform.anchorMin = icoMinAnchor; _pauseIcon.rectTransform.anchorMin = icoMinAnchor; _closeIcon.rectTransform.anchorMin = icoMinAnchor; _togglLabel.gameObject.SetActive(_showButtonLabels); _canvsLabel.gameObject.SetActive(_showButtonLabels); _clearLabel.gameObject.SetActive(_showButtonLabels); _pauseLabel.gameObject.SetActive(_showButtonLabels); _closeLabel.gameObject.SetActive(_showButtonLabels); var rect = CurrentRect; _rootPanelRT.anchorMax = new Vector2(rect.xMax, rect.yMax); _rootPanelRT.anchorMin = new Vector2(rect.xMin, rect.yMin); _rootPanelRT.sizeDelta = new Vector2(0.0f, 0.0f); _rootPanelRT.pivot = new Vector2(0.5f, 0.5f); _rootPanelRT.anchoredPosition3D = default; _headerRT.anchorMin = new Vector2(0.0f, 1); _headerRT.anchorMax = new Vector2(1.0f, 1); _headerRT.pivot = new Vector2(0.5f, 1); _headerRT.anchoredPosition3D = default; _headerRT.sizeDelta = new Vector2(0, /*TITLE_HEIGHT +*/ maxHeaderHeight); _objectTitlePanelRT.offsetMax = new Vector2(-MARGIN, -MARGIN); _objectTitlePanelRT.offsetMin = new Vector2(MARGIN, -(ObjectTitleHeight)); _objectIdsGroupRT.offsetMax = new Vector2(-MARGIN, -(ObjectTitleHeight + MARGIN)); _objectIdsGroupRT.offsetMin = new Vector2(MARGIN, -(ObjectTitleHeight + ObjectIdsHeight)); _objectMetersPanelRT.offsetMax = new Vector2(-MARGIN, -(ObjectTitleHeight + ObjectIdsHeight + MARGIN)); _objectMetersPanelRT.offsetMin = new Vector2(MARGIN, -(ObjectTitleHeight + ObjectIdsHeight + ObjectMetersHeight)); // Disable object sections that have been minimized to 0 _objectTitlePanelRT.gameObject.SetActive(EnableObjectStats && ObjectTitleHeight > 0); _objectIdsGroupRT.gameObject.SetActive(EnableObjectStats && ObjectIdsHeight > 0); _objectMetersPanelRT.gameObject.SetActive(EnableObjectStats && ObjectMetersHeight > 0); _statsPanelRT.ExpandAnchor().SetOffsets(0, 0, 0, -(/*TITLE_HEIGHT + */maxHeaderHeight)); if (_enableObjectStats && _statsPanelRT.rect.height < (ObjectTitleHeight + ObjectIdsHeight + ObjectMetersHeight)) { _statsPanelRT.offsetMin = new Vector2(0.0f, _statsPanelRT.rect.height - (ObjectTitleHeight + ObjectIdsHeight + ObjectMetersHeight + MARGIN)); } var graphColCount = GraphColumnCount > 0 ? GraphColumnCount : (int)(_graphsLayoutRT.rect.width / (_graphMaxWidth + MARGIN)); if (graphColCount < 1) { graphColCount = 1; } var graphRowCount = (int)Math.Ceiling((double)_foundGraphs.Count / graphColCount); if (graphRowCount < 1) { graphRowCount = 1; } if (graphRowCount == 1) { graphColCount = _foundGraphs.Count; } _graphGridLayoutGroup.constraint = UnityEngine.UI.GridLayoutGroup.Constraint.FixedColumnCount; _graphGridLayoutGroup.constraintCount = graphColCount; var cellwidth = _graphsLayoutRT.rect.width / graphColCount - MARGIN; var cellheight = _graphsLayoutRT.rect.height / graphRowCount - (/*(graphRowCount - 1) **/ MARGIN); _graphGridLayoutGroup.cellSize = new Vector2(cellwidth, cellheight); _graphsLayoutRT.offsetMax = new Vector2(0, _enableObjectStats ? -(ObjectTitleHeight + ObjectIdsHeight + ObjectMetersHeight + MARGIN) : -MARGIN); if (_foundViews == null) { _foundViews = new List(GetComponentsInChildren(false)); } else { GetComponentsInChildren(false, _foundViews); } if (_objGraphs != null) { // enabled/disable any object graphs based on _enabledObjectStats setting foreach (var objGraph in _objGraphs) { if (objGraph) { objGraph.gameObject.SetActive((_includedObjStats.Mask & ((long)1 << objGraph.StatId)) != 0 && _enableObjectStats); } } } for (int i = 0; i < _foundViews.Count; ++i) { var graph = _foundViews[i]; if (graph == null || graph.isActiveAndEnabled == false) { continue; } graph.CalculateLayout(); graph.transform.localRotation = default; graph.transform.localScale = new Vector3(1, 1, 1); } } } void ApplyDefaultLayout(DefaultLayouts defaults, StatCanvasTypes? applyForCanvasType = null) { bool applyToGO = applyForCanvasType.HasValue == false || applyForCanvasType.Value == StatCanvasTypes.GameObject; bool applyToOL = applyForCanvasType.HasValue == false || applyForCanvasType.Value == StatCanvasTypes.Overlay; if (defaults == DefaultLayouts.Custom) { return; } Rect screenrect; Rect objectrect; bool isTall; #if UNITY_EDITOR var currentRes = UnityEditor.Handles.GetMainGameViewSize(); isTall = (currentRes.y > currentRes.x); #else isTall = Screen.height > Screen.width; #endif switch (defaults) { case DefaultLayouts.Left: { objectrect = Rect.MinMaxRect(0.0f, 0.0f, 0.3f, 1.0f); screenrect = objectrect; break; } case DefaultLayouts.Right: { objectrect = Rect.MinMaxRect(0.7f, 0.0f, 1.0f, 1.0f); screenrect = objectrect; break; } case DefaultLayouts.UpperLeft: { objectrect = Rect.MinMaxRect(0.0f, 0.5f, 0.3f, 1.0f); screenrect = isTall ? Rect.MinMaxRect(0.0f, 0.7f, 0.3f, 1.0f) : objectrect; break; } case DefaultLayouts.UpperRight: { objectrect = Rect.MinMaxRect(0.7f, 0.5f, 1.0f, 1.0f); screenrect = isTall ? Rect.MinMaxRect(0.7f, 0.7f, 1.0f, 1.0f) : objectrect; break; } case DefaultLayouts.Full: { objectrect = Rect.MinMaxRect(0.0f, 0.0f, 1.0f, 1.0f); screenrect = objectrect; break; } default: { objectrect = Rect.MinMaxRect(0.0f, 0.5f, 0.3f, 1.0f); screenrect = objectrect; break; } } if (applyToGO) { GameObjectRect = objectrect; } if (applyToOL) { OverlayRect = screenrect; } _layoutDirty += 1; } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStats.Statics.cs namespace Fusion { using System.Collections.Generic; using UnityEngine; public partial class FusionStats { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void ResetStatics() { _statsForRunnerLookup.Clear(); _activeGuids.Clear(); _newInputSystemFound = null; } // Lookup for all FusionStats associated with active runners. static Dictionary> _statsForRunnerLookup = new Dictionary>(); // Record of active SimStats, used to prevent more than one _guid version from existing (in the case of SimStats existing in a scene that gets cloned in Multi-Peer). static Dictionary _activeGuids = new Dictionary(); public static NetworkId MonitoredNetworkObjectId { get; set; } /// /// Any FusionStats instance with NetworkObject stats enabled, will use this Object when that FusionStats.Object is null. /// /// public static void SetMonitoredNetworkObject(NetworkObject no) { if (no) { MonitoredNetworkObjectId = no.Id; } else { MonitoredNetworkObjectId = new NetworkId(); } } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStatsExtensions.cs namespace Fusion.StatsInternal { using System.Collections.Generic; using UnityEngine; using System; using System.ComponentModel; using System.Reflection; // [Flags] // public enum FusionGraphVisualization { // [Description("Auto")] // Auto, // [Description("Continuous Tick")] // ContinuousTick = 1, // [Description("Intermittent Tick")] // IntermittentTick = 2, // [Description("Intermittent Time")] // IntermittentTime = 4, // [Description("Value Histogram")] // ValueHistogram = 8, // [Description("Count Histogram")] // CountHistogram = 16, // } // /// /// Engine sources for Samples. /// public enum StatSourceTypes { Simulation, NetworkObject, NetConnection, Behaviour, } // [Flags] // public enum StatsPer { // Individual = 1, // Tick = 2, // Second = 4, // } [Flags] public enum StatFlags { ValidOnServer = 1 << 0, ValidOnClient = 1 << 1, ValidInShared = 1 << 2, ValidOnStateAuthority = 1 << 5, ValidForBuildType = 1 << 6, } public static class FusionStatsExtensions { public struct FieldMaskData { public GUIContent[] LongNames; public GUIContent[] ShortNames; public StatsMetaAttribute[] Metas; public FieldInfo[] FieldInfos; public Mask256 DefaultMask; } private static Dictionary s_lookup = new (); private static FieldMaskData Lookup(this Type type) { if (type == typeof(SimulationStats)) return Lookup(StatSourceTypes.Simulation); if (type == typeof(BehaviourStats)) return Lookup(StatSourceTypes.Behaviour); if (type == typeof(SimulationConnectionStats)) return Lookup(StatSourceTypes.NetConnection); if (type == typeof(NetworkObjectStats)) return Lookup(StatSourceTypes.NetworkObject); return default; } public static Mask256 GetDefaults(this Type type) { return type.Lookup().DefaultMask; } /// /// Get the cached Long Name for the stat source and type. /// public static string GetLongName(this StatSourceTypes type, int statId) { var data = Lookup(type); return data.LongNames[statId].text; } private static string GetRuntimeNicifiedName(this StatsMetaAttribute meta, FieldInfo fieldInfo) { if (meta.Name != null) { return meta.Name; } // TODO: Make this return formatting closer to Unity's Object.Nicify return System.Text.RegularExpressions.Regex.Replace(fieldInfo.Name, "(?<=[a-z])([A-Z])", " $1").Trim(); } public static FieldMaskData Lookup(this StatSourceTypes type) { if (s_lookup.TryGetValue(type, out var fieldMaskData)) { return fieldMaskData; } var fields = type.GetStateSourceType().GetFields(); var gcLong = new GUIContent[fields.Length]; var gcShort = new GUIContent[fields.Length]; var metas = new StatsMetaAttribute[fields.Length]; var defaults = new Mask256(); for (int i = 0; i < fields.Length; ++i) { metas[i] = fields[i].GetCustomAttribute(); var longName = metas[i].GetRuntimeNicifiedName(fields[i]); gcLong[i] = new GUIContent(longName); gcShort[i] = new GUIContent(metas[i].ShortName); if (metas[i].DefaultEnabled) { defaults.SetBit(i, true); } } var data = new FieldMaskData() { LongNames = gcLong, ShortNames = gcShort, Metas = metas, FieldInfos = fields, DefaultMask = defaults}; s_lookup.Add(type, data); return data; } private static Type GetStateSourceType(this StatSourceTypes statSourceType) { switch (statSourceType) { case StatSourceTypes.Behaviour: return typeof(BehaviourStats); case StatSourceTypes.NetConnection: return typeof(SimulationConnectionStats); case StatSourceTypes.Simulation: return typeof(SimulationStats); case StatSourceTypes.NetworkObject: return typeof(NetworkObjectStats); } Debug.LogError($"Stat Type not found."); return default; } public static (StatsMetaAttribute meta, FieldInfo fieldInfo) GetDescription(this StatSourceTypes statSource, int statId) { var datas = Lookup(statSource); return (datas.Metas[statId], datas.FieldInfos[statId]); } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStatsGraphBase.cs namespace Fusion { using System.Reflection; using UnityEngine; using UI = UnityEngine.UI; using StatsInternal; using UnityEngine.Serialization; [ScriptHelp(BackColor = ScriptHeaderBackColor.Olive)] public abstract class FusionStatsGraphBase : Fusion.Behaviour, IFusionStatsView { protected const int PAD = FusionStatsUtilities.PAD; protected const int MRGN = FusionStatsUtilities.MARGIN; // protected const int MAX_FONT_SIZE_WITH_GRAPH = 24; [SerializeField] [HideInInspector] protected UI.Text LabelTitle; [SerializeField] [HideInInspector] protected UI.Image BackImage; /// /// Which section of the Fusion engine is being monitored. In combination with StatId, this selects the stat being monitored. /// [InlineHelp] [SerializeField] protected StatSourceTypes _statSourceType; public StatSourceTypes StateAuthorityType { get => _statSourceType; set { _statSourceType = value; TryConnect(); } } /// /// The specific stat being monitored. /// [InlineHelp] [SerializeField] //[DisplayAsEnum(nameof(CastToStatType))] protected int _statId = -1; public int StatId { get => _statId; set { _statId = value; TryConnect(); } } // [FormerlySerializedAs("StatsPerDefault")] [InlineHelp] // public StatAveraging StatsAvergingDefault; [InlineHelp] public float WarnThreshold; [InlineHelp] public float ErrorThreshold; // protected IStatsBuffer _statsBuffer; // public IStatsBuffer StatsBuffer { // get { // if (_statsBuffer == null) { // TryConnect(); // } // return _statsBuffer; // } // } protected bool _isOverlay; public bool IsOverlay { set { if (_isOverlay != value) { _isOverlay = value; CalculateLayout(); _layoutDirty = _layoutDirty < 1 ? 1 : _layoutDirty; } } get { return _isOverlay; } } // Needed for multi-peer, cloned graphs otherwise have default layout. private void Start() { _layoutDirty = 4; } protected virtual Color BackColor { get { if (_statSourceType == StatSourceTypes.Simulation) { return FusionStats.SimDataBackColor; } if (_statSourceType == StatSourceTypes.NetConnection) { return FusionStats.NetDataBackColor; } return FusionStats.ObjDataBackColor; } } // protected Type CastToStatType => // (_statSourceType == StatSourceTypes.Simulation) ? typeof(Stats.SimStats) : // (_statSourceType == StatSourceTypes.NetConnection) ? typeof(Stats.NetStats) : // typeof(Stats.ObjStats); [SerializeField] protected FusionStats _fusionStats; protected FusionStats FusionStats { get { if (_fusionStats) return _fusionStats; return _fusionStats = GetComponentInParent(); } } // protected FusionStats LocateParentFusionStats() { // if (_fusionStats == null) { // _fusionStats = GetComponentInParent(); // } // return _fusionStats; // } protected int _layoutDirty = 2; [SerializeField] protected StatAveraging CurrentAveraging; // public StatSourceInfo StatSourceInfo; protected StatsMetaAttribute _statSourceInfo; public StatsMetaAttribute StatSourceInfo { get { if (_statSourceInfo == null) { TryConnect(); } return _statSourceInfo; } } private FieldInfo _fieldInfo; public FieldInfo FieldInfo { get { if (_fieldInfo == null) { TryConnect(); } return _fieldInfo; } } private object _statsObject; private NetworkObject _previousNetworkObject; protected object StatsObject { get { if (_statsObject != null && _previousNetworkObject == _fusionStats.Object) { return _statsObject; } var runner = FusionStats.Runner; if (runner.IsRunning) { switch (_statSourceType) { case StatSourceTypes.Simulation: { runner.TryGetSimulationStats(out var stats); return _statsObject = stats; } case StatSourceTypes.NetworkObject: { var no = _fusionStats.Object; // GetComponentInParent(); if (no != _previousNetworkObject) { if (no) { if (runner.TryGetObjectStats(no.Id, out var stats)) { _previousNetworkObject = no; } return _statsObject = stats; } _previousNetworkObject = null; return _statsObject = default; } return _statsObject; } case StatSourceTypes.NetConnection: { runner.TryGetPlayerStats(FusionStats.PlayerRef, out var stats); // if (stats == null) // Debug.LogError($"Failed to get Stats Object for netConnection {_fusionStats.Runner.Mode} {_fusionStats.PlayerRef}"); return _statsObject = stats; } // Need to write handling for all other stat types } } Debug.LogError($"Can't get stats object {_statSourceType}"); return default; } } // Track source values to detect changes in OnValidate. [SerializeField] [HideInInspector] StatSourceTypes _prevStatSourceType; [SerializeField] [HideInInspector] int _prevStatId; #if UNITY_EDITOR protected virtual void OnValidate() { if (_statSourceType != _prevStatSourceType || _statId != _prevStatId) { WarnThreshold = 0; ErrorThreshold = 0; _prevStatSourceType = _statSourceType; _prevStatId = _statId; } } #endif public virtual void Initialize() { } protected virtual void CyclePer() { switch (CurrentAveraging) { case StatAveraging.PerSample: // Only include PerSecond if that was the original default handling. Otherwise we assume per second is not a useful graph. if (StatSourceInfo.Averaging == StatAveraging.PerSecond) { CurrentAveraging = StatAveraging.PerSecond; } else { CurrentAveraging = StatAveraging.Latest; } return; case StatAveraging.PerSecond: CurrentAveraging = StatAveraging.Latest; return; case StatAveraging.Latest: CurrentAveraging = StatAveraging.RecentPeak; return; case StatAveraging.RecentPeak: CurrentAveraging = StatAveraging.Peak; return; case StatAveraging.Peak: CurrentAveraging = StatAveraging.PerSample; return; } } public abstract void CalculateLayout(); public abstract void Refresh(); public void Disconnect() { _statsObject = null; } protected virtual bool TryConnect() { // Don't try to connect if values are not initialized. (was just added as a component and OnValidate is calling this method). if (_statId == -1) { return false; } var info = FusionStatsExtensions.GetDescription(_statSourceType, _statId); _statSourceInfo = info.meta; _fieldInfo = info.fieldInfo; if (WarnThreshold == 0 && ErrorThreshold == 0) { WarnThreshold = StatSourceInfo.WarnThreshold; ErrorThreshold = StatSourceInfo.ErrorThreshold; } if (gameObject.activeInHierarchy == false) { return false; } // // Any data connection requires a runner for the statistics source. // // TODO: Is this needed still? // var runner = FusionStats?.Runner; if (BackImage) { BackImage.color = BackColor; } // Update the labels, regardless if a connection can be made. if (LabelTitle) { ApplyTitleText(); } // If averaging setting is not set yet, get the default. if (CurrentAveraging == 0) { CurrentAveraging = _statSourceInfo.Averaging; } return true; } protected void ApplyTitleText() { var info = StatSourceInfo; if (info == null) { return; } string longName = _statSourceType.GetLongName(_statId); if (!LabelTitle) { return; } var titleRT = LabelTitle.rectTransform; if (titleRT.rect.width < 100) { LabelTitle.text = info.ShortName ?? longName; } else { LabelTitle.text = longName; } BackImage.gameObject.SetActive(true); } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionStatsUtilities.cs namespace Fusion.StatsInternal { using UnityEngine; using UnityEngine.Events; using UI = UnityEngine.UI; public interface IFusionStatsView { void Initialize(); void CalculateLayout(); void Refresh(); bool isActiveAndEnabled { get; } Transform transform { get; } } public static class FusionStatsUtilities { public const int PAD = 10; public const int MARGIN = 6; public const int FONT_SIZE = 12; public const int FONT_SIZE_MIN = 4; public const int FONT_SIZE_MAX = 200; const int METER_TEXTURE_WIDTH = 512; static Texture2D _meterTexture; static Texture2D MeterTexture { get { if (_meterTexture == null) { var tex = new Texture2D(METER_TEXTURE_WIDTH, 2); for (int x = 0; x < METER_TEXTURE_WIDTH; ++x) { for (int y = 0; y < 2; ++y) { var color = (x != 0 && x % 16 == 0) ? new Color(1f, 1f, 1f, 0.75f) : new Color(1f, 1f, 1f, 1f); tex.SetPixel(x, y, color); } } tex.Apply(); return _meterTexture = tex; } return _meterTexture; } } static Sprite _meterSprite; public static Sprite MeterSprite { get { if (_meterSprite == null) { _meterSprite = Sprite.Create(MeterTexture, new Rect(0, 0, METER_TEXTURE_WIDTH, 2), new Vector2()); } return _meterSprite; } } const int R = 64; static Texture2D _circle32Texture; static Texture2D Circle32Texture { get { if (_circle32Texture == null) { var tex = new Texture2D(R * 2, R * 2); for (int x = 0; x < R; ++x) { for (int y = 0; y < R; ++y) { double h = System.Math.Abs( System.Math.Sqrt(x * x + y * y)); float a = h > R ? 0.0f : h < (R - 1) ? 1.0f :(float) (R - h); var c = new Color(1.0f, 1.0f, 1.0f, a); tex.SetPixel(R + 0 + x, R + 0 + y, c); tex.SetPixel(R - 1 - x, R + 0 + y, c); tex.SetPixel(R + 0 + x, R - 1 - y, c); tex.SetPixel(R - 1 - x, R - 1 - y, c); } } tex.Apply(); return _circle32Texture = tex; } return _circle32Texture; } } static Sprite _circle32Sprite; public static Sprite CircleSprite { get { if (_circle32Sprite == null) { _circle32Sprite = Sprite.Create(Circle32Texture, new Rect(0, 0, R * 2, R * 2), new Vector2(R , R), 10f, 0, SpriteMeshType.Tight, new Vector4(R-1, R-1, R-1, R-1)); } return _circle32Sprite; } } public static Color DARK_GREEN = new Color(0.0f, 0.5f, 0.0f, 1.0f); public static Color DARK_BLUE = new Color(0.0f, 0.0f, 0.5f, 1.0f); public static Color DARK_RED = new Color(0.5f, 0.0f, 0.0f, 1.0f); public static void ValidateRunner(this FusionStats fusionStats, NetworkRunner currentRunner) { bool runnerFromSelected = fusionStats.RunnerFromSelected; bool currentRunnerIsValid = currentRunner && currentRunner.IsRunning; // Logic: // If EnableObjectStats is set, then always prioritize Object's runner. // next if RunnerFromSelected == true, try to get runner from selected object // next Find first active runner that matches ConnectTo - If not enforce single, bias toward runner associated with the FusionStats itself // First check to see if the current runner is perfectly valid so we can skip any searching expenses if (currentRunnerIsValid && runnerFromSelected == false && fusionStats.EnableObjectStats == false) { if ((fusionStats.ConnectTo & currentRunner.Mode) != 0) { return; } } // Prioritize selected NetworkObjects if EnableObjectStats if (fusionStats.EnableObjectStats) { var obj = fusionStats.Object; if (obj) { fusionStats.SetRunner(obj.Runner); return; } } // If in the editor and using Selected, #if UNITY_EDITOR if (runnerFromSelected) { var selected = UnityEditor.Selection.activeObject as GameObject; if (selected) { var found = NetworkRunner.GetRunnerForGameObject(selected); if (found && found.IsRunning) { fusionStats.SetRunner(found); return; } } } #endif var gameObject = fusionStats.gameObject; var connectTo = fusionStats.ConnectTo; // If we are no enforcing single, bias toward the runner associated with this actual FusionStats GameObject. if (fusionStats.EnforceSingle == false) { var sceneRunner = NetworkRunner.GetRunnerForGameObject(gameObject); if (sceneRunner && sceneRunner.IsRunning && (sceneRunner.Mode & connectTo) != 0) { fusionStats.SetRunner(sceneRunner); return; } } // Finally Loop all runners, looking for one that matches connectTo var enumerator = NetworkRunner.GetInstancesEnumerator(); while (enumerator.MoveNext()) { var found = enumerator.Current; // Ignore non-running if (found == null || found.IsRunning == false) { continue; } // May as well stop if we find Single Player, there is only one. if (found.IsSinglePlayer) { fusionStats.SetRunner(found); return; } // If this runner matches our preferred runner (ConnectTo), use it. if ((connectTo & found.Mode) != 0) { fusionStats.SetRunner(found); return; } } } public static RectTransform CreateRectTransform(this Transform parent, string name, bool expand = false) { var go = new GameObject(name); var rt = go.AddComponent(); rt.SetParent(parent); rt.localPosition = default; rt.localScale = default; rt.localScale = new Vector3(1, 1, 1); if (expand) { ExpandAnchor(rt); } return rt; } public static UI.Text AddText(this RectTransform rt, string label, TextAnchor anchor, Color FontColor, Font font, int maxFontSize = 200) { var text = rt.gameObject.AddComponent(); if (font != null) { text.font = font; } text.text = label; text.color = FontColor; text.alignment = anchor; text.fontSize = FONT_SIZE; text.raycastTarget = false; text.resizeTextMinSize = FONT_SIZE_MIN; text.resizeTextMaxSize = maxFontSize; text.resizeTextForBestFit = true; return text; } public const float BTTN_LBL_NORM_HGHT = .175f; private const int BTTN_FONT_SIZE_MAX = 100; private const float BTTN_ALPHA = 0.925f; internal static void MakeButton(this RectTransform parent, ref UI.Button button, string iconText, string labelText, Font font, out UI.Text icon, out UI.Text text, UnityAction action) { var rt = parent.CreateRectTransform(labelText); button = rt.gameObject.AddComponent(); var iconRt = rt.CreateRectTransform("Icon", true); iconRt.anchorMin = new Vector2(0, BTTN_LBL_NORM_HGHT); iconRt.anchorMax = new Vector2(1, 1.0f); iconRt.offsetMin = new Vector2(0, 0); iconRt.offsetMax = new Vector2(0, 0); icon = iconRt.gameObject.AddComponent(); button.targetGraphic = icon; if (font != null) { icon.font = font; } icon.text = iconText; icon.alignment = TextAnchor.MiddleCenter; icon.fontStyle = FontStyle.Bold; icon.fontSize = BTTN_FONT_SIZE_MAX; icon.resizeTextMinSize = 0; icon.resizeTextMaxSize = BTTN_FONT_SIZE_MAX; icon.alignByGeometry = true; icon.resizeTextForBestFit = true; var textRt = rt.CreateRectTransform("Label", true); textRt.anchorMin = new Vector2(0, 0); textRt.anchorMax = new Vector2(1, BTTN_LBL_NORM_HGHT); textRt.pivot = new Vector2(.5f, BTTN_LBL_NORM_HGHT * .5f); textRt.offsetMin = new Vector2(0, 0); textRt.offsetMax = new Vector2(0, 0); text = textRt.gameObject.AddComponent(); text.color = Color.black; if (font != null) { text.font = font; } text.text = labelText; text.alignment = TextAnchor.MiddleCenter; text.fontStyle = FontStyle.Bold; text.fontSize = 0; text.resizeTextMinSize = 0; text.resizeTextMaxSize = BTTN_FONT_SIZE_MAX; text.resizeTextForBestFit = true; text.horizontalOverflow = HorizontalWrapMode.Overflow; UI.ColorBlock colors = button.colors; colors.normalColor = new Color(.0f, .0f, .0f, BTTN_ALPHA); colors.pressedColor = new Color(.5f, .5f, .5f, BTTN_ALPHA); colors.highlightedColor = new Color(.3f, .3f, .3f, BTTN_ALPHA); colors.selectedColor = new Color(.0f, .0f, .0f, BTTN_ALPHA); button.colors = colors; button.onClick.AddListener(action); } public static RectTransform AddVerticalLayoutGroup(this RectTransform rt, float spacing, int? rgtPad = null, int? lftPad = null, int? topPad = null, int? botPad = null) { var group = rt.gameObject.AddComponent(); group.childControlHeight = true; group.childControlWidth = true; group.spacing = spacing; return rt; } public static UI.GridLayoutGroup AddGridlLayoutGroup(this RectTransform rt, float spacing, int? rgtPad = null, int? lftPad = null, int? topPad = null, int? botPad = null) { var group = rt.gameObject.AddComponent(); group.spacing = new Vector2( spacing, spacing); return group; } public static RectTransform AddImage(this RectTransform rt, Color color) { var image = rt.gameObject.AddComponent(); image.color = color; image.raycastTarget = false; return rt; } public static RectTransform AddCircleSprite(this RectTransform rt, Color color) { rt.AddCircleSprite(color, out var _); return rt; } public static RectTransform AddCircleSprite(this RectTransform rt, Color color, out UI.Image image) { image = rt.gameObject.AddComponent(); image.sprite = CircleSprite; image.type = UI.Image.Type.Sliced; image.pixelsPerUnitMultiplier = 100f; image.color = color; image.raycastTarget = false; return rt; } public static RectTransform ExpandAnchor(this RectTransform rt, float? padding = null) { rt.anchorMax = new Vector2(1, 1); rt.anchorMin = new Vector2(0, 0); rt.pivot = new Vector2(0.5f, 0.5f); if (padding.HasValue) { rt.offsetMin = new Vector2(padding.Value, padding.Value); rt.offsetMax = new Vector2(-padding.Value, -padding.Value); } else { rt.sizeDelta = default; rt.anchoredPosition = default; } return rt; } public static RectTransform ExpandTopAnchor(this RectTransform rt, float? padding = null) { rt.anchorMax = new Vector2(1, 1); rt.anchorMin = new Vector2(0, 1); rt.pivot = new Vector2(0.5f, 1f); if (padding.HasValue) { rt.offsetMin = new Vector2(padding.Value, padding.Value); rt.offsetMax = new Vector2(-padding.Value, -padding.Value); } else { rt.sizeDelta = default; rt.anchoredPosition = default; } return rt; } public static RectTransform SetSizeDelta(this RectTransform rt, float offsetX, float offsetY) { rt.sizeDelta = new Vector2(offsetX, offsetY); return rt; } public static RectTransform SetOffsets(this RectTransform rt, float minX, float maxX, float minY, float maxY) { rt.offsetMin = new Vector2(minX, minY); rt.offsetMax = new Vector2(maxX, maxY); return rt; } public static RectTransform SetPivot(this RectTransform rt, float pivotX, float pivotY) { rt.pivot = new Vector2(pivotX, pivotY); return rt; } public static RectTransform SetAnchors(this RectTransform rt, float minX, float maxX, float minY, float maxY) { rt.anchorMin = new Vector2(minX, minY); rt.anchorMax = new Vector2(maxX, maxY); return rt; } } } #endregion #region Assets/Photon/Fusion/Runtime/FusionUnityLogger.cs namespace Fusion { using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; using UnityEditor; using UnityEngine; using UnityEngine.Serialization; using Object = UnityEngine.Object; [Serializable] public partial class FusionUnityLogger : Fusion.ILogger { /// /// Implement this to modify values of this logger. /// /// static partial void InitializePartial(ref FusionUnityLogger logger); StringBuilder _builder = new StringBuilder(); Thread _mainThread; public string NameUnavailableObjectDestroyedLabel = "(destroyed)"; public string NameUnavailableInWorkerThreadLabel = ""; /// /// If true, all messages will be prefixed with [Fusion] tag /// public bool UseGlobalPrefix; /// /// If true, some parts of messages will be enclosed with <color> tags. /// public bool UseColorTags; /// /// If true, each log message that has a source parameter will be prefixed with a hash code of the source object. /// public bool AddHashCodePrefix; /// /// Color of the global prefix (see ). /// public string GlobalPrefixColor; /// /// Min Random Color /// public Color32 MinRandomColor; /// /// Max Random Color /// public Color32 MaxRandomColor; /// /// Server Color /// public Color ServerColor; public FusionUnityLogger(Thread mainThread) { _mainThread = mainThread; bool isDarkMode = false; #if UNITY_EDITOR isDarkMode = UnityEditor.EditorGUIUtility.isProSkin; #endif MinRandomColor = isDarkMode ? new Color32(158, 158, 158, 255) : new Color32(30, 30, 30, 255); MaxRandomColor = isDarkMode ? new Color32(255, 255, 255, 255) : new Color32(90, 90, 90, 255); ServerColor = isDarkMode ? new Color32(255, 255, 158, 255) : new Color32(30, 90, 200, 255); UseColorTags = true; UseGlobalPrefix = true; GlobalPrefixColor = Color32ToRGBString(isDarkMode ? new Color32(115, 172, 229, 255) : new Color32(20, 64, 120, 255)); } public void Log(LogType logType, object message, in LogContext logContext) { Debug.Assert(_builder.Length == 0); string fullMessage; var obj = logContext.Source as UnityEngine.Object; try { if (logType == LogType.Debug) { _builder.Append("[DEBUG] "); } else if (logType == LogType.Trace) { _builder.Append("[TRACE] "); } if (UseGlobalPrefix) { if (UseColorTags) { _builder.Append(""); } _builder.Append("[Fusion"); if (!string.IsNullOrEmpty(logContext.Prefix)) { _builder.Append("/"); _builder.Append(logContext.Prefix); } _builder.Append("]"); if (UseColorTags) { _builder.Append(""); } _builder.Append(" "); } else { if (!string.IsNullOrEmpty(logContext.Prefix)) { _builder.Append(logContext.Prefix); _builder.Append(": "); } } if (obj) { var pos = _builder.Length; if (obj is NetworkRunner runner) { TryAppendRunnerPrefix(_builder, runner); } else if (obj is NetworkObject networkObject) { TryAppendNetworkObjectPrefix(_builder, networkObject); } else if (obj is SimulationBehaviour simulationBehaviour) { TryAppendSimulationBehaviourPrefix(_builder, simulationBehaviour); } else { AppendNameThreadSafe(_builder, obj); } if (_builder.Length > pos) { _builder.Append(": "); } } _builder.Append(message); fullMessage = _builder.ToString(); } finally { _builder.Clear(); } switch (logType) { case LogType.Error: Debug.LogError(fullMessage, IsInMainThread ? obj : null); break; case LogType.Warn: Debug.LogWarning(fullMessage, IsInMainThread ? obj : null); break; default: Debug.Log(fullMessage, IsInMainThread ? obj : null); break; } } public void LogException(Exception ex, in LogContext logContext) { Log(LogType.Error, $"{ex.GetType()} See next error log entry for details.", in logContext); #if UNITY_EDITOR // this is to force console window double click to take you where the exception // has been thrown, not where it has been logged var edi = ExceptionDispatchInfo.Capture(ex); var thread = new Thread(() => { edi.Throw(); }); thread.Start(); thread.Join(); #else if (logContext.Source is UnityEngine.Object obj) { Debug.LogException(ex, obj); } else { Debug.LogException(ex); } #endif } int GetRandomColor(int seed) => GetRandomColor(seed, MinRandomColor, MaxRandomColor, ServerColor); int GetColorSeed(string name) { int hash = 0; for (var i = 0; i < name.Length; ++i) { hash = hash * 31 + name[i]; } return hash; } static int GetRandomColor(int seed, Color32 min, Color32 max, Color32 svr) { var random = new NetworkRNG(seed); int r, g, b; // -1 indicates host/client - give it a more pronounced color. if (seed == -1) { r = svr.r; g = svr.g; b = svr.b; } else { r = random.RangeInclusive(min.r, max.r); g = random.RangeInclusive(min.g, max.g); b = random.RangeInclusive(min.b, max.b); } r = Mathf.Clamp(r, 0, 255); g = Mathf.Clamp(g, 0, 255); b = Mathf.Clamp(b, 0, 255); int rgb = (r << 16) | (g << 8) | b; return rgb; } static int Color32ToRGB24(Color32 c) { return (c.r << 16) | (c.g << 8) | c.b; } static string Color32ToRGBString(Color32 c) { return string.Format("#{0:X6}", Color32ToRGB24(c)); } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Initialize() { if (Fusion.Log.Initialized) { return; } var logger = new FusionUnityLogger(Thread.CurrentThread); // Optional override of default values InitializePartial(ref logger); if (logger != null) { Fusion.Log.Init(logger); } } private void AppendNameThreadSafe(StringBuilder builder, UnityEngine.Object obj) { if ((object)obj == null) throw new ArgumentNullException(nameof(obj)); string name; bool isDestroyed = obj == null; if (isDestroyed) { name = NameUnavailableObjectDestroyedLabel; } else if (!IsInMainThread) { name = NameUnavailableInWorkerThreadLabel; } else { name = obj.name; } if (UseColorTags) { int colorSeed = GetColorSeed(name); builder.AppendFormat("", GetRandomColor(colorSeed)); } if (AddHashCodePrefix) { builder.AppendFormat("{0:X8}", obj.GetHashCode()); } if (name?.Length > 0) { if (AddHashCodePrefix) { builder.Append(" "); } builder.Append(name); } if (UseColorTags) { builder.Append(""); } } private bool IsInMainThread => _mainThread == Thread.CurrentThread; bool TryAppendRunnerPrefix(StringBuilder builder, NetworkRunner runner) { if ((object)runner == null) { return false; } if (runner.Config?.PeerMode != NetworkProjectConfig.PeerModes.Multiple) { return false; } AppendNameThreadSafe(builder, runner); var localPlayer = runner.LocalPlayer; if (localPlayer.IsRealPlayer) { builder.Append("[P").Append(localPlayer.PlayerId).Append("]"); } else { builder.Append("[P-]"); } return true; } bool TryAppendNetworkObjectPrefix(StringBuilder builder, NetworkObject networkObject) { if ((object)networkObject == null) { return false; } AppendNameThreadSafe(builder, networkObject); if (networkObject.Id.IsValid) { builder.Append(" "); builder.Append(networkObject.Id.ToString()); } int pos = builder.Length; if (TryAppendRunnerPrefix(builder, networkObject.Runner)) { builder.Insert(pos, '@'); } return true; } bool TryAppendSimulationBehaviourPrefix(StringBuilder builder, SimulationBehaviour simulationBehaviour) { if ((object)simulationBehaviour == null) { return false; } AppendNameThreadSafe(builder, simulationBehaviour); if (simulationBehaviour is NetworkBehaviour nb && nb.Id.IsValid) { builder.Append(" "); builder.Append(nb.Id.ToString()); } int pos = builder.Length; if (TryAppendRunnerPrefix(builder, simulationBehaviour.Runner)) { builder.Insert(pos, '@'); } return true; } } } #endregion #region Assets/Photon/Fusion/Runtime/NetworkObjectBaker.cs //#undef UNITY_EDITOR namespace Fusion { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public class NetworkObjectBaker { private List _allNetworkObjects = new List(); private List _networkObjectsPaths = new List(); private List _allSimulationBehaviours = new List(); private TransformPathCache _pathCache = new TransformPathCache(); private List _arrayBufferNB = new List(); private List _arrayBufferNO = new List(); public struct Result { public bool HadChanges { get; } public int ObjectCount { get; } public int BehaviourCount { get; } public Result(bool dirty, int objectCount, int behaviourCount) { HadChanges = dirty; ObjectCount = objectCount; BehaviourCount = behaviourCount; } } protected virtual void SetDirty(MonoBehaviour obj) { // do nothing } protected virtual bool TryGetExecutionOrder(MonoBehaviour obj, out int order) { order = default; return false; } protected virtual uint GetSortKey(NetworkObject obj) { return 0; } [System.Diagnostics.Conditional("FUSION_EDITOR_TRACE")] protected static void Trace(string msg) { Debug.Log($"[Fusion/NetworkObjectBaker] {msg}"); } protected static void Warn(string msg, UnityEngine.Object context = null) { Debug.LogWarning($"[Fusion/NetworkObjectBaker] {msg}", context); } public Result Bake(GameObject root) { if (root == null) { throw new ArgumentNullException(nameof(root)); } root.GetComponentsInChildren(true, _allNetworkObjects); // remove null ones (missing scripts may cause that) _allNetworkObjects.RemoveAll(x => x == null); if (_allNetworkObjects.Count == 0) { return new Result(false, 0, 0); } try { foreach (var obj in _allNetworkObjects) { _networkObjectsPaths.Add(_pathCache.Create(obj.transform)); } bool dirty = false; _allNetworkObjects.Reverse(); _networkObjectsPaths.Reverse(); root.GetComponentsInChildren(true, _allSimulationBehaviours); _allSimulationBehaviours.RemoveAll(x => x == null); int countNO = _allNetworkObjects.Count; int countSB = _allSimulationBehaviours.Count; // start from the leaves for (int i = 0; i < _allNetworkObjects.Count; ++i) { var obj = _allNetworkObjects[i]; var objDirty = false; var objActive = obj.gameObject.activeInHierarchy; int? objExecutionOrder = null; if (!objActive) { if (TryGetExecutionOrder(obj, out var order)) { objExecutionOrder = order; } else { Warn($"Unable to get execution order for {obj}. " + $"Because the object is initially inactive, Fusion is unable to guarantee " + $"the script's Awake will be invoked before Spawned. Please implement {nameof(TryGetExecutionOrder)}."); } } // find nested behaviours _arrayBufferNB.Clear(); var path = _networkObjectsPaths[i]; string entryPath = path.ToString(); for (int scriptIndex = _allSimulationBehaviours.Count - 1; scriptIndex >= 0; --scriptIndex) { var script = _allSimulationBehaviours[scriptIndex]; var scriptPath = _pathCache.Create(script.transform); if (_pathCache.IsEqualOrAncestorOf(path, scriptPath)) { if (script is NetworkBehaviour nb) { _arrayBufferNB.Add(nb); } _allSimulationBehaviours.RemoveAt(scriptIndex); if (objExecutionOrder != null) { // check if execution order is ok if (TryGetExecutionOrder(script, out var scriptOrder)) { if (objExecutionOrder <= scriptOrder) { Warn($"{obj} execution order is less or equal than of the script {script}. " + $"Because the object is initially inactive, Spawned callback will be invoked before the script's Awake on activation.", script); } } else { Warn($"Unable to get execution order for {script}. " + $"Because the object is initially inactive, Fusion is unable to guarantee " + $"the script's Awake will be invoked before Spawned. Please implement {nameof(TryGetExecutionOrder)}."); } } } else if (_pathCache.Compare(path, scriptPath) < 0) { // can't discard it yet } else { Debug.Assert(_pathCache.Compare(path, scriptPath) > 0); break; } } _arrayBufferNB.Reverse(); objDirty |= Set(obj, ref obj.NetworkedBehaviours, _arrayBufferNB); // handle flags var flags = obj.Flags; if (!flags.IsVersionCurrent()) { flags = flags.SetCurrentVersion(); } objDirty |= Set(obj, ref obj.Flags, flags); // what's left is nested network objects resolution { _arrayBufferNO.Clear(); // collect descendants; descendants should be continous without gaps here int j = i - 1; for (; j >= 0 && _pathCache.IsAncestorOf(path, _networkObjectsPaths[j]); --j) { _arrayBufferNO.Add(_allNetworkObjects[j]); } int descendantsBegin = j + 1; Debug.Assert(_arrayBufferNO.Count == i - descendantsBegin); objDirty |= Set(obj, ref obj.NestedObjects, _arrayBufferNO); } objDirty |= Set(obj, ref obj.SortKey, GetSortKey(obj)); if (objDirty) { SetDirty(obj); dirty = true; } } return new Result(dirty, countNO, countSB); } finally { _pathCache.Clear(); _allNetworkObjects.Clear(); _allSimulationBehaviours.Clear(); _networkObjectsPaths.Clear(); _arrayBufferNB.Clear(); _arrayBufferNO.Clear(); } } private bool Set(MonoBehaviour host, ref T field, T value) { if (!EqualityComparer.Default.Equals(field, value)) { Trace($"Object dirty: {host} ({field} vs {value})"); field = value; return true; } else { return false; } } private bool Set(MonoBehaviour host, ref T[] field, List value) { var comparer = EqualityComparer.Default; if (field == null || field.Length != value.Count || !field.SequenceEqual(value, comparer)) { Trace($"Object dirty: {host} ({field} vs {value})"); field = value.ToArray(); return true; } else { return false; } } public unsafe readonly struct TransformPath { public const int MaxDepth = 10; public struct _Indices { public fixed ushort Value[MaxDepth]; } public readonly _Indices Indices; public readonly ushort Depth; public readonly ushort Next; internal TransformPath(ushort depth, ushort next, List indices, int offset, int count) { Depth = depth; Next = next; for (int i = 0; i < count; ++i) { Indices.Value[i] = indices[i + offset]; } } public override string ToString() { var builder = new StringBuilder(); for (int i = 0; i < Depth && i < MaxDepth; ++i) { if (i > 0) { builder.Append("/"); } builder.Append(Indices.Value[i]); } if (Depth > MaxDepth) { Debug.Assert(Next > 0); builder.Append($"/...[{Depth - MaxDepth}]"); } return builder.ToString(); } } public sealed unsafe class TransformPathCache : IComparer, IEqualityComparer { private Dictionary _cache = new Dictionary(); private List _siblingIndexStack = new List(); private List _nexts = new List(); public TransformPath Create(Transform transform) { if (_cache.TryGetValue(transform, out var existing)) { return existing; } _siblingIndexStack.Clear(); for (var tr = transform; tr != null; tr = tr.parent) { _siblingIndexStack.Add(checked((ushort)tr.GetSiblingIndex())); } _siblingIndexStack.Reverse(); var depth = checked((ushort)_siblingIndexStack.Count); ushort nextPlusOne = 0; if (depth > TransformPath.MaxDepth) { int i; if (depth % TransformPath.MaxDepth != 0) { // tail is going to be partially full i = depth - (depth % TransformPath.MaxDepth); } else { // tail is going to be full i = depth - TransformPath.MaxDepth; } for (; i > 0; i -= TransformPath.MaxDepth) { checked { TransformPath path = new TransformPath((ushort)(depth - i), nextPlusOne, _siblingIndexStack, i, Mathf.Min(TransformPath.MaxDepth, depth - i)); _nexts.Add(path); nextPlusOne = (ushort)_nexts.Count; } } } var result = new TransformPath(depth, nextPlusOne, _siblingIndexStack, 0, Mathf.Min(TransformPath.MaxDepth, depth)); _cache.Add(transform, result); return result; } public void Clear() { _nexts.Clear(); _cache.Clear(); _siblingIndexStack.Clear(); } public bool Equals(TransformPath x, TransformPath y) { if (x.Depth != y.Depth) { return false; } return CompareToDepthUnchecked(x, y, x.Depth) == 0; } public int GetHashCode(TransformPath obj) { int hash = obj.Depth; return GetHashCode(obj, hash); } public int Compare(TransformPath x, TransformPath y) { var diff = CompareToDepthUnchecked(x, y, Mathf.Min(x.Depth, y.Depth)); if (diff != 0) { return diff; } return x.Depth - y.Depth; } private int CompareToDepthUnchecked(in TransformPath x, in TransformPath y, int depth) { for (int i = 0; i < depth && i < TransformPath.MaxDepth; ++i) { int diff = x.Indices.Value[i] - y.Indices.Value[i]; if (diff != 0) { return diff; } } if (depth > TransformPath.MaxDepth) { Debug.Assert(x.Next > 0); Debug.Assert(y.Next > 0); return CompareToDepthUnchecked(_nexts[x.Next - 1], _nexts[y.Next - 1], depth - TransformPath.MaxDepth); } else { return 0; } } private int GetHashCode(in TransformPath path, int hash) { for (int i = 0; i < path.Depth && i < TransformPath.MaxDepth; ++i) { hash = hash * 31 + path.Indices.Value[i]; } if (path.Depth > TransformPath.MaxDepth) { Debug.Assert(path.Next > 0); hash = GetHashCode(_nexts[path.Next - 1], hash); } return hash; } public bool IsAncestorOf(in TransformPath x, in TransformPath y) { if (x.Depth >= y.Depth) { return false; } return CompareToDepthUnchecked(x, y, x.Depth) == 0; } public bool IsEqualOrAncestorOf(in TransformPath x, in TransformPath y) { if (x.Depth > y.Depth) { return false; } return CompareToDepthUnchecked(x, y, x.Depth) == 0; } public string Dump(in TransformPath x) { var builder = new StringBuilder(); Dump(x, builder); return builder.ToString(); } private void Dump(in TransformPath x, StringBuilder builder) { for (int i = 0; i < x.Depth && i < TransformPath.MaxDepth; ++i) { if (i > 0) { builder.Append("/"); } builder.Append(x.Indices.Value[i]); } if (x.Depth > TransformPath.MaxDepth) { Debug.Assert(x.Next > 0); builder.Append("/"); Dump(_nexts[x.Next - 1], builder); } } } } } #endregion #region Assets/Photon/Fusion/Runtime/NetworkPrefabSourceUnity.cs namespace Fusion { using System; using Object = UnityEngine.Object; [Serializable] public class NetworkPrefabSourceStatic : NetworkAssetSourceStatic, INetworkPrefabSource { public NetworkObjectGuid AssetGuid; NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid; } [Serializable] public class NetworkPrefabSourceStaticLazy : NetworkAssetSourceStaticLazy, INetworkPrefabSource { public NetworkObjectGuid AssetGuid; NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid; } [Serializable] public class NetworkPrefabSourceResource : NetworkAssetSourceResource, INetworkPrefabSource { public NetworkObjectGuid AssetGuid; NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid; } #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES [Serializable] public class NetworkPrefabSourceAddressable : NetworkAssetSourceAddressable, INetworkPrefabSource { public NetworkObjectGuid AssetGuid; NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid; } #endif } #endregion #region Assets/Photon/Fusion/Runtime/Utilities/FusionScalableIMGUI.cs namespace Fusion { using System.Reflection; using UnityEngine; /// /// In-Game IMGUI style used for the interface. /// public static class FusionScalableIMGUI { private static GUISkin _scalableSkin; private static void InitializedGUIStyles(GUISkin baseSkin) { _scalableSkin = baseSkin == null ? GUI.skin : baseSkin; // If no skin was provided, make the built in GuiSkin more tolerable. if (baseSkin == null) { _scalableSkin = GUI.skin; _scalableSkin.button.alignment = TextAnchor.MiddleCenter; _scalableSkin.label.alignment = TextAnchor.MiddleCenter; _scalableSkin.textField.alignment = TextAnchor.MiddleCenter; _scalableSkin.button.normal.background = _scalableSkin.box.normal.background; _scalableSkin.button.hover.background = _scalableSkin.window.normal.background; _scalableSkin.button.normal.textColor = new Color(.8f, .8f, .8f); _scalableSkin.button.hover.textColor = new Color(1f, 1f, 1f); _scalableSkin.button.active.textColor = new Color(1f, 1f, 1f); _scalableSkin.button.border = new RectOffset(6, 6, 6, 6); _scalableSkin.window.border = new RectOffset(8, 8, 8, 10); } else { // Use the supplied skin as the base. _scalableSkin = baseSkin; } } /// /// Get the custom scalable skin, already resized to the current screen. Provides the height, width, padding and margin used. /// /// public static GUISkin GetScaledSkin(GUISkin baseSkin, out float height, out float width, out int padding, out int margin, out float boxLeft) { if (_scalableSkin == null) { InitializedGUIStyles(baseSkin); } var dimensions = ScaleGuiSkinToScreenHeight(); height = dimensions.Item1; width = dimensions.Item2; padding = dimensions.Item3; margin = dimensions.Item4; boxLeft = dimensions.Item5; return _scalableSkin; } /// /// Modifies a skin to make it scale with screen height. /// /// /// Returns (height, width, padding, top-margin, left-box-margin) values applied to the GuiSkin public static (float, float, int, int, float) ScaleGuiSkinToScreenHeight() { bool isVerticalAspect = Screen.height > Screen.width; bool isSuperThin = Screen.height / Screen.width > (17f / 9f); float height = Screen.height * .08f; float width = System.Math.Min(Screen.width * .9f, Screen.height * .6f); int padding = (int)(height / 4); int margin = (int)(height / 8); float boxLeft = (Screen.width - width) * .5f; int fontsize = (int)(isSuperThin ? (width - (padding * 2)) * .07f : height * .4f); var margins = new RectOffset(0, 0, margin, margin); _scalableSkin.button.fontSize = fontsize; _scalableSkin.button.margin = margins; _scalableSkin.label.fontSize = fontsize; _scalableSkin.label.padding = new RectOffset(padding, padding, padding, padding); _scalableSkin.textField.fontSize = fontsize; _scalableSkin.window.padding = new RectOffset(padding, padding, padding, padding); _scalableSkin.window.margin = new RectOffset(margin, margin, margin, margin); return (height, width, padding, margin, boxLeft); } } } #endregion #region Assets/Photon/Fusion/Runtime/Utilities/FusionUnitySceneManagerUtils.cs namespace Fusion { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; public static class FusionUnitySceneManagerUtils { public class SceneEqualityComparer : IEqualityComparer { public bool Equals(Scene x, Scene y) { return x.handle == y.handle; } public int GetHashCode(Scene obj) { return obj.handle; } } public static bool IsAddedToBuildSettings(this Scene scene) { if (scene.buildIndex < 0) { return false; } // yep that's a thing: https://docs.unity3d.com/ScriptReference/SceneManagement.Scene-buildIndex.html if (scene.buildIndex >= SceneManager.sceneCountInBuildSettings) { return false; } return true; } #if UNITY_EDITOR public static bool AddToBuildSettings(Scene scene) { if (IsAddedToBuildSettings(scene)) { return false; } EditorBuildSettings.scenes = new[] { new EditorBuildSettingsScene(scene.path, true) } .Concat(EditorBuildSettings.scenes) .ToArray(); Debug.Log($"Added '{scene.path}' as first entry in Build Settings."); return true; } #endif public static LocalPhysicsMode GetLocalPhysicsMode(this Scene scene) { LocalPhysicsMode mode = LocalPhysicsMode.None; if (scene.GetPhysicsScene() != Physics.defaultPhysicsScene) { mode |= LocalPhysicsMode.Physics3D; } if (scene.GetPhysicsScene2D() != Physics2D.defaultPhysicsScene) { mode |= LocalPhysicsMode.Physics2D; } return mode; } /// /// Finds all components of type in the scene. /// /// /// /// /// public static T[] GetComponents(this Scene scene, bool includeInactive) where T : Component { return GetComponents(scene, includeInactive, out _); } /// /// Finds all components of type in the scene. /// /// /// /// /// /// public static T[] GetComponents(this Scene scene, bool includeInactive, out GameObject[] rootObjects) where T : Component { rootObjects = scene.GetRootGameObjects(); var partialResult = new List(); var result = new List(); foreach (var go in rootObjects) { // depth-first, according to docs and verified by our tests go.GetComponentsInChildren(includeInactive: includeInactive, partialResult); // AddRange accepts IEnumerable, so there would be an alloc foreach (var comp in partialResult) { result.Add(comp); } } return result.ToArray(); } private static readonly List _reusableGameObjectList = new List(); /// /// Finds all components of type in the scene. /// /// /// /// /// /// public static void GetComponents(this Scene scene, List results, bool includeInactive) where T : Component { var rootObjects = _reusableGameObjectList; scene.GetRootGameObjects(rootObjects); results.Clear(); var partialResult = new List(); foreach (var go in rootObjects) { // depth-first, according to docs and verified by our tests go.GetComponentsInChildren(includeInactive: includeInactive, partialResult); // AddRange accepts IEnumerable, so there would be an alloc foreach (var comp in partialResult) { results.Add(comp); } } } /// /// Finds the first instance of type in the scene. Returns null if no instance found. /// /// /// /// /// public static T FindComponent(this Scene scene, bool includeInactive = false) where T : Component { var rootObjects = _reusableGameObjectList; scene.GetRootGameObjects(rootObjects); foreach (var go in rootObjects) { // depth-first, according to docs and verified by our tests var found = go.GetComponentInChildren(includeInactive); if (found != null) { return found; } } return null; } public static bool CanBeUnloaded(this Scene scene) { if (!scene.isLoaded) { return false; } for (int i = 0; i < SceneManager.sceneCount; ++i) { var s = SceneManager.GetSceneAt(i); if (s != scene && s.isLoaded) { return true; } } return false; } public static string Dump(this Scene scene) { StringBuilder result = new StringBuilder(); result.Append("[UnityScene:"); if (scene.IsValid()) { result.Append(scene.name); result.Append(", isLoaded:").Append(scene.isLoaded); result.Append(", buildIndex:").Append(scene.buildIndex); result.Append(", isDirty:").Append(scene.isDirty); result.Append(", path:").Append(scene.path); result.Append(", rootCount:").Append(scene.rootCount); result.Append(", isSubScene:").Append(scene.isSubScene); } else { result.Append(""); } result.Append(", handle:").Append(scene.handle); result.Append("]"); return result.ToString(); } public static string Dump(this LoadSceneParameters loadSceneParameters) { return $"[LoadSceneParameters: {loadSceneParameters.loadSceneMode}, localPhysicsMode:{loadSceneParameters.localPhysicsMode}]"; } public static int GetSceneBuildIndex(string nameOrPath) { if (nameOrPath.IndexOf('/') >= 0) { return SceneUtility.GetBuildIndexByScenePath(nameOrPath); } else { for (int i = 0; i < SceneManager.sceneCountInBuildSettings; ++i) { var scenePath = SceneUtility.GetScenePathByBuildIndex(i); GetFileNameWithoutExtensionPosition(scenePath, out var nameIndex, out var nameLength); if (nameLength == nameOrPath.Length && string.Compare(scenePath, nameIndex, nameOrPath, 0, nameLength, true) == 0) { return i; } } return -1; } } public static int GetSceneIndex(IList scenePathsOrNames, string nameOrPath) { if (nameOrPath.IndexOf('/') >= 0) { return scenePathsOrNames.IndexOf(nameOrPath); } else { for (int i = 0; i < scenePathsOrNames.Count; ++i) { var scenePath = scenePathsOrNames[i]; GetFileNameWithoutExtensionPosition(scenePath, out var nameIndex, out var nameLength); if (nameLength == nameOrPath.Length && string.Compare(scenePath, nameIndex, nameOrPath, 0, nameLength, true) == 0) { return i; } } return -1; } } public static void GetFileNameWithoutExtensionPosition(string nameOrPath, out int index, out int length) { var lastSlash = nameOrPath.LastIndexOf('/'); if (lastSlash >= 0) { index = lastSlash + 1; } else { index = 0; } var lastDot = nameOrPath.LastIndexOf('.'); if (lastDot > index) { length = lastDot - index; } else { length = nameOrPath.Length - index; } } } } #endregion #region Assets/Photon/Fusion/Runtime/Utilities/RunnerVisibility/NetworkRunnerVisibilityExtensions.cs namespace Fusion { using System.Collections.Generic; using UnityEngine; using Analyzer; public static class NetworkRunnerVisibilityExtensions { // TODO: Still needed? [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetAllSimulationStatics() { ResetStatics(); } /// /// Types that fusion.runtime isn't aware of, which need to be found using names instead. /// [StaticField(StaticFieldResetMode.None)] private static readonly string[] RecognizedBehaviourNames = { "EventSystem" }; [StaticField(StaticFieldResetMode.None)] private static readonly System.Type[] RecognizedBehaviourTypes = { typeof(IRunnerVisibilityRecognizedType), typeof(Renderer), typeof(AudioListener), typeof(Camera), typeof(Canvas), typeof(Light) }; private static readonly Dictionary DictionaryLookup; // Constructor static NetworkRunnerVisibilityExtensions() { DictionaryLookup = new Dictionary(); } private class RunnerVisibility { public bool IsVisible { get; set; } = true; public LinkedList Nodes = new LinkedList(); } public static void EnableVisibilityExtension(this NetworkRunner runner) { if (runner && DictionaryLookup.ContainsKey(runner) == false) { DictionaryLookup.Add(runner, new RunnerVisibility()); } } public static void DisableVisibilityExtension(this NetworkRunner runner) { if (runner && DictionaryLookup.ContainsKey(runner)) { DictionaryLookup.Remove(runner); } } public static bool HasVisibilityEnabled(this NetworkRunner runner) { return DictionaryLookup.ContainsKey(runner); } public static bool GetVisible(this NetworkRunner runner) { if (runner == null) { return false; } if (DictionaryLookup.TryGetValue(runner, out var runnerVisibility) == false) { return true; } return runnerVisibility.IsVisible; } public static void SetVisible(this NetworkRunner runner, bool isVisibile) { runner.GetVisibilityInfo().IsVisible = isVisibile; RefreshRunnerVisibility(runner); } private static LinkedList GetVisibilityNodes(this NetworkRunner runner) { if (runner == false) { return null; } return runner.GetVisibilityInfo()?.Nodes; } private static RunnerVisibility GetVisibilityInfo(this NetworkRunner runner) { if (DictionaryLookup.TryGetValue(runner, out var runnerVisibility) == false) { return null; } return runnerVisibility; } /// /// Find all component types that contribute to a scene rendering, and associate them with a component, /// and add them to the runner's list of visibility nodes. /// /// /// public static void AddVisibilityNodes(this NetworkRunner runner, GameObject go) { runner.EnableVisibilityExtension(); // Check for flag component which indicates object has already been cataloged. if (go.GetComponent()) {return;} go.AddComponent(); // Have user EnableOnSingleRunner add RunnerVisibilityControl before we process all nodes. var existingEnableOnSingles = go.transform.GetComponentsInChildren(false); foreach (var enableOnSingleRunner in existingEnableOnSingles) { enableOnSingleRunner.AddNodes(); } RunnerVisibilityLink[] existingNodes = go.GetComponentsInChildren(false); CollectBehavioursAndAddNodes(go, runner, existingNodes); RefreshRunnerVisibility(runner); } private static void CollectBehavioursAndAddNodes(GameObject go, NetworkRunner runner, RunnerVisibilityLink[] existingNodes) { // If any changes are made to the commons, we need a full refresh. var commonsNeedRefresh = false; var components = go.transform.GetComponentsInChildren(true); foreach (var comp in components) { var nodeAlreadyExists = false; // Check for broken/missing components if (comp == null) continue; // See if devs added a node for this behaviour already foreach (var existingNode in existingNodes) if (existingNode.Component == comp) { nodeAlreadyExists = true; AddNodeToCommonLookup(existingNode); RegisterNode(existingNode, runner, comp); commonsNeedRefresh = true; break; } if (nodeAlreadyExists) continue; // No existing node was found, create one if this comp is a recognized render type var type = comp.GetType(); // Only add if comp is one of the behaviours considered render related. foreach (var recognizedType in RecognizedBehaviourTypes) if (IsRecognizedByRunnerVisibility(type)) { var node = comp.gameObject.AddComponent(); RegisterNode(node, runner, comp); break; } } if (commonsNeedRefresh) RefreshCommonObjectVisibilities(); } internal static bool IsRecognizedByRunnerVisibility(this System.Type type) { // First try the faster type based lookup foreach (var recognizedType in RecognizedBehaviourTypes) { if (recognizedType.IsAssignableFrom(type)) return true; } // The try the slower string based (for namespace references not included in the Fusion core). var typename = type.Name; foreach (var recognizedNames in RecognizedBehaviourNames) { if (typename.Contains(recognizedNames)) return true; } return false; } private static void RegisterNode(RunnerVisibilityLink link, NetworkRunner runner, Component comp) { // #if DEBUG // if (runner.GetVisibilityNodes().Contains(node)) // Log.Warn($"{nameof(RunnerVisibilityNode)} on '{node.name}' already has been registered."); // #endif var listnode = runner.GetVisibilityNodes().AddLast(link); link.Initialize(comp, runner, listnode); } public static void UnregisterNode(this RunnerVisibilityLink link) { if (link == null || link._runner == null) { return; } var runner = link._runner; var runnerIsNullOrDestroyed = !(runner); if (!runnerIsNullOrDestroyed) { var visNodes = link._runner.GetVisibilityNodes(); if (visNodes == null) { // No VisibilityNodes collection, likely a shutdown condition. return; } } if (runnerIsNullOrDestroyed == false && runner.GetVisibilityNodes().Contains(link)) { runner.GetVisibilityNodes().Remove(link); } // // Remove from the Runner list. // if (!ReferenceEquals(node, null) && node._node != null && node._node.List != null) { // node._node.List.Remove(node); // } if (link.Guid != null) { if (CommonObjectLookup.TryGetValue(link.Guid, out var clones)) { if (clones.Contains(link)) { clones.Remove(link); } // if this is the last instance of this _guid... remove the entry from the lookup. if (clones.Count == 0) { CommonObjectLookup.Remove(link.Guid); } } } } private static void AddNodeToCommonLookup(RunnerVisibilityLink link) { var guid = link.Guid; if (string.IsNullOrEmpty(guid)) return; if (!CommonObjectLookup.TryGetValue(guid, out var clones)) { clones = new List(); CommonObjectLookup.Add(guid, clones); } clones.Add(link); } /// /// Reapplies a runner's IsVisibile setting to all of its registered visibility nodes. /// /// /// private static void RefreshRunnerVisibility(NetworkRunner runner, bool refreshCommonObjects = true) { // Trying to refresh before the runner has setup. if (runner.GetVisibilityNodes() == null) { //Log.Warn($"{nameof(NetworkRunner)} visibility can't be changed. Not ready yet."); return; } bool enable = runner.GetVisible(); foreach (var node in runner.GetVisibilityNodes()) { // This should never be null, but just in case... if (node == null) { continue; } node.SetEnabled(enable); } if (refreshCommonObjects) { RefreshCommonObjectVisibilities(); } } /// /// Dictionary lookup for manually added visibility nodes (which indicates only one instance should be visible at a time), /// which returns a list of nodes for a given LocalIdentifierInFile. /// [StaticField] private readonly static Dictionary> CommonObjectLookup = new Dictionary>(); internal static void RefreshCommonObjectVisibilities() { var runners = NetworkRunner.GetInstancesEnumerator(); NetworkRunner serverRunner = null; NetworkRunner clientRunner = null; NetworkRunner inputAuthority = null; // First find the runner for each preference. while (runners.MoveNext()) { var runner = runners.Current; // Exclude inactive runners TODO: may not be needed after this list is patched to contain only active if (!runner.IsRunning || !runner.GetVisible() || runner.IsShutdown) continue; if (runner.IsServer) serverRunner = runner; if (!clientRunner && runner.IsClient) { clientRunner = runner; } if (!inputAuthority && runner.ProvideInput) { inputAuthority = runner; } } // If the preferred runner isn't available for some types, pick a runner as a fallback. if (!serverRunner) serverRunner = inputAuthority ? inputAuthority : clientRunner; if (!(clientRunner)) clientRunner = (serverRunner) ? serverRunner : inputAuthority; if (!(inputAuthority)) inputAuthority = (serverRunner) ? serverRunner : clientRunner; // loop all common objects, making sure to activate only one peer instance. foreach (var kvp in CommonObjectLookup) { var clones = kvp.Value; if (clones.Count > 0) { NetworkRunner prefRunner; switch (clones[0].PreferredRunner) { case RunnerVisibilityLink.PreferredRunners.Server: prefRunner = serverRunner; break; case RunnerVisibilityLink.PreferredRunners.Client: prefRunner = clientRunner; break; case RunnerVisibilityLink.PreferredRunners.InputAuthority: prefRunner = inputAuthority; break; default: prefRunner = null; break; } foreach (var clone in clones) { clone.Enabled = ReferenceEquals(clone._runner, prefRunner); } } } } [StaticFieldResetMethod] internal static void ResetStatics() { CommonObjectLookup.Clear(); } } } #endregion #endif