You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3777 lines
121 KiB
C#

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

#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<T> 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<UnityEngine.Object>();
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<T>();
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<T> 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<T>(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<T>(ResourcePath) : LoadNamedResource(ResourcePath, SubObjectName);
} else {
_state = UnityResources.LoadAsync<T>(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<T>(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<T> 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<T> where T : UnityEngine.Object {
[FormerlySerializedAs("Prefab")]
public LazyLoadReference<T> Object;
[Obsolete("Use Object instead")]
public LazyLoadReference<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.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<FusionGlobalScriptableObject>(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<FusionGlobalScriptableObjectAttribute>();
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<IAsyncOperation> _completed;
private float _progress;
private Action _activateAsync;
public FusionCoroutine(IEnumerator inner) {
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
public event Action<IAsyncOperation> 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<float> InterpolationOffset = new ProfilerCounter<float>(Category, "Interp Offset", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InterpolationTimeScale = new ProfilerCounter<float>(Category, "Interp Time Scale", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InterpolationMultiplier = new ProfilerCounter<float>(Category, "Interp Multiplier", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InterpolationUncertainty = new ProfilerCounter<float>(Category, "Interp Uncertainty", ProfilerMarkerDataUnit.Undefined);
public static readonly ProfilerCounter<int> InputSize = new ProfilerCounter<int>(Category, "Client Input Size", ProfilerMarkerDataUnit.Bytes);
public static readonly ProfilerCounter<int> InputQueue = new ProfilerCounter<int>(Category, "Client Input Queue", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<int> WorldSnapshotSize = new ProfilerCounter<int>(Category, "Client Snapshot Size", ProfilerMarkerDataUnit.Bytes);
public static readonly ProfilerCounter<int> Resimulations = new ProfilerCounter<int>(Category, "Client Resims", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> RoundTripTime = new ProfilerCounter<float>(Category, "Client RTT", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounterValue<int> RpcIn = new ProfilerCounterValue<int>(Category, "RPCs In", ProfilerMarkerDataUnit.Count, ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);
public static readonly ProfilerCounterValue<int> RpcOut = new ProfilerCounterValue<int>(Category, "RPCs Out", ProfilerMarkerDataUnit.Count, ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);
public static readonly ProfilerCounter<float> SimulationTimeScale = new ProfilerCounter<float>(Category, "Simulation Time Scale", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InputOffset = new ProfilerCounter<float>(Category, "Input Offset", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InputOffsetDeviation = new ProfilerCounter<float>(Category, "Input Offset Dev", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InputRecvDelta = new ProfilerCounter<float>(Category, "Input Recv Delta", ProfilerMarkerDataUnit.Count);
public static readonly ProfilerCounter<float> InputRecvDeltaDeviation = new ProfilerCounter<float>(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<FusionStatsBillboard>();
fs.AddComponent<FusionStats>();
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<IFusionStatsView> _foundViews;
[NonSerialized] List<FusionStatsGraph> _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;
/// <summary>
/// Creates a new GameObject with a <see cref="FusionStats"/> component, attaches it to any supplied parent, and generates Canvas/Graphs.
/// </summary>
/// <param name="runner"></param>
/// <param name="parent">Generated FusionStats component and GameObject will be added as a child of this transform.</param>
/// <param name="objectLayout">Uses a predefined position.</param>
/// <param name="netStatsMask">The network stats to be enabled. If left null, default statistics will be used.</param>
/// <param name="simStatsMask">The simulation stats to be enabled. If left null, default statistics will be used.</param>
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<FusionStats>();
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<Transform>();
_canvasRT = rootRectTr.CreateRectTransform("Stats Canvas");
_canvas = _canvasRT.gameObject.AddComponent<Canvas>();
_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<UnityEngine.UI.CanvasScaler>();
scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(SCREEN_SCALE_W, SCREEN_SCALE_H);
scaler.matchWidthOrHeight = .4f;
_canvasRT.gameObject.AddComponent<UnityEngine.UI.GraphicRaycaster>();
_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<UnityEngine.UI.Button>();
_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<UnityEngine.UI.HorizontalLayoutGroup>();
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<UnityEngine.UI.Button>();
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<FusionStatsGraph>(_graphsLayoutRT.GetComponentsInChildren<FusionStatsGraph>(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<FusionStatsBillboard>() == 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<IFusionStatsView>(GetComponentsInChildren<IFusionStatsView>(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<NetworkRunner, List<FusionStats>> _statsForRunnerLookup = new Dictionary<NetworkRunner, List<FusionStats>>();
// 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<string, FusionStats> _activeGuids = new Dictionary<string, FusionStats>();
public static NetworkId MonitoredNetworkObjectId { get; set; }
/// <summary>
/// Any FusionStats instance with NetworkObject stats enabled, will use this Object when that FusionStats.Object is null.
/// </summary>
/// <param name="no"></param>
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,
// }
//
/// <summary>
/// Engine sources for Samples.
/// </summary>
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<StatSourceTypes, FieldMaskData> 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;
}
/// <summary>
/// Get the cached Long Name for the stat source and type.
/// </summary>
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<StatsMetaAttribute>();
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;
/// <summary>
/// Which section of the Fusion engine is being monitored. In combination with StatId, this selects the stat being monitored.
/// </summary>
[InlineHelp]
[SerializeField]
protected StatSourceTypes _statSourceType;
public StatSourceTypes StateAuthorityType {
get => _statSourceType;
set {
_statSourceType = value;
TryConnect();
}
}
/// <summary>
/// The specific stat being monitored.
/// </summary>
[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<FusionStats>();
}
}
// protected FusionStats LocateParentFusionStats() {
// if (_fusionStats == null) {
// _fusionStats = GetComponentInParent<FusionStats>();
// }
// 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<NetworkObject>();
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<RectTransform>();
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<UI.Text>();
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<UI.Button>();
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<UI.Text>();
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<UI.Text>();
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<UI.VerticalLayoutGroup>();
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<UI.GridLayoutGroup>();
group.spacing = new Vector2( spacing, spacing);
return group;
}
public static RectTransform AddImage(this RectTransform rt, Color color) {
var image = rt.gameObject.AddComponent<UI.Image>();
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<UI.Image>();
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 {
/// <summary>
/// Implement this to modify values of this logger.
/// </summary>
/// <param name="logger"></param>
static partial void InitializePartial(ref FusionUnityLogger logger);
StringBuilder _builder = new StringBuilder();
Thread _mainThread;
public string NameUnavailableObjectDestroyedLabel = "(destroyed)";
public string NameUnavailableInWorkerThreadLabel = "";
/// <summary>
/// If true, all messages will be prefixed with [Fusion] tag
/// </summary>
public bool UseGlobalPrefix;
/// <summary>
/// If true, some parts of messages will be enclosed with &lt;color&gt; tags.
/// </summary>
public bool UseColorTags;
/// <summary>
/// If true, each log message that has a source parameter will be prefixed with a hash code of the source object.
/// </summary>
public bool AddHashCodePrefix;
/// <summary>
/// Color of the global prefix (see <see cref="UseGlobalPrefix"/>).
/// </summary>
public string GlobalPrefixColor;
/// <summary>
/// Min Random Color
/// </summary>
public Color32 MinRandomColor;
/// <summary>
/// Max Random Color
/// </summary>
public Color32 MaxRandomColor;
/// <summary>
/// Server Color
/// </summary>
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("<color=");
_builder.Append(GlobalPrefixColor);
_builder.Append(">");
}
_builder.Append("[Fusion");
if (!string.IsNullOrEmpty(logContext.Prefix)) {
_builder.Append("/");
_builder.Append(logContext.Prefix);
}
_builder.Append("]");
if (UseColorTags) {
_builder.Append("</color>");
}
_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()} <i>See next error log entry for details.</i>", 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("<color=#{0:X6}>", 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("</color>");
}
}
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<NetworkObject> _allNetworkObjects = new List<NetworkObject>();
private List<TransformPath> _networkObjectsPaths = new List<TransformPath>();
private List<SimulationBehaviour> _allSimulationBehaviours = new List<SimulationBehaviour>();
private TransformPathCache _pathCache = new TransformPathCache();
private List<NetworkBehaviour> _arrayBufferNB = new List<NetworkBehaviour>();
private List<NetworkObject> _arrayBufferNO = new List<NetworkObject>();
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<T>(MonoBehaviour host, ref T field, T value) {
if (!EqualityComparer<T>.Default.Equals(field, value)) {
Trace($"Object dirty: {host} ({field} vs {value})");
field = value;
return true;
} else {
return false;
}
}
private bool Set<T>(MonoBehaviour host, ref T[] field, List<T> value) {
var comparer = EqualityComparer<T>.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<ushort> 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<TransformPath>, IEqualityComparer<TransformPath> {
private Dictionary<Transform, TransformPath> _cache = new Dictionary<Transform, TransformPath>();
private List<ushort> _siblingIndexStack = new List<ushort>();
private List<TransformPath> _nexts = new List<TransformPath>();
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<NetworkObject>, INetworkPrefabSource {
public NetworkObjectGuid AssetGuid;
NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid;
}
[Serializable]
public class NetworkPrefabSourceStaticLazy : NetworkAssetSourceStaticLazy<NetworkObject>, INetworkPrefabSource {
public NetworkObjectGuid AssetGuid;
NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid;
}
[Serializable]
public class NetworkPrefabSourceResource : NetworkAssetSourceResource<NetworkObject>, INetworkPrefabSource {
public NetworkObjectGuid AssetGuid;
NetworkObjectGuid INetworkPrefabSource.AssetGuid => AssetGuid;
}
#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
[Serializable]
public class NetworkPrefabSourceAddressable : NetworkAssetSourceAddressable<NetworkObject>, 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;
/// <summary>
/// In-Game IMGUI style used for the <see cref="FusionBootstrapDebugGUI"/> interface.
/// </summary>
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;
}
}
/// <summary>
/// Get the custom scalable skin, already resized to the current screen. Provides the height, width, padding and margin used.
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Modifies a skin to make it scale with screen height.
/// </summary>
/// <param name="skin"></param>
/// <returns>Returns (height, width, padding, top-margin, left-box-margin) values applied to the GuiSkin</returns>
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<Scene> {
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;
}
/// <summary>
/// Finds all components of type <typeparam name="T"/> in the scene.
/// </summary>
/// <param name="scene"></param>
/// <param name="includeInactive"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T[] GetComponents<T>(this Scene scene, bool includeInactive) where T : Component {
return GetComponents<T>(scene, includeInactive, out _);
}
/// <summary>
/// Finds all components of type <typeparam name="T"/> in the scene.
/// </summary>
/// <param name="scene"></param>
/// <param name="includeInactive"></param>
/// <param name="rootObjects"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T[] GetComponents<T>(this Scene scene, bool includeInactive, out GameObject[] rootObjects) where T : Component {
rootObjects = scene.GetRootGameObjects();
var partialResult = new List<T>();
var result = new List<T>();
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<GameObject> _reusableGameObjectList = new List<GameObject>();
/// <summary>
/// Finds all components of type <typeparam name="T"/> in the scene.
/// </summary>
/// <param name="scene"></param>
/// <param name="results"></param>
/// <param name="includeInactive"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static void GetComponents<T>(this Scene scene, List<T> results, bool includeInactive) where T : Component {
var rootObjects = _reusableGameObjectList;
scene.GetRootGameObjects(rootObjects);
results.Clear();
var partialResult = new List<T>();
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);
}
}
}
/// <summary>
/// Finds the first instance of type <typeparam name="T"/> in the scene. Returns null if no instance found.
/// </summary>
/// <param name="scene"></param>
/// <param name="includeInactive"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T FindComponent<T>(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<T>(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("<Invalid>");
}
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<string> 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();
}
/// <summary>
/// Types that fusion.runtime isn't aware of, which need to be found using names instead.
/// </summary>
[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<NetworkRunner, RunnerVisibility> DictionaryLookup;
// Constructor
static NetworkRunnerVisibilityExtensions() {
DictionaryLookup = new Dictionary<NetworkRunner, RunnerVisibility>();
}
private class RunnerVisibility {
public bool IsVisible { get; set; } = true;
public LinkedList<RunnerVisibilityLink> Nodes = new LinkedList<RunnerVisibilityLink>();
}
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<RunnerVisibilityLink> 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;
}
/// <summary>
/// Find all component types that contribute to a scene rendering, and associate them with a <see cref="RunnerVisibilityLink"/> component,
/// and add them to the runner's list of visibility nodes.
/// </summary>
/// <param name="go"></param>
/// <param name="runner"></param>
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<RunnerVisibilityLinksRoot>()) {return;}
go.AddComponent<RunnerVisibilityLinksRoot>();
// Have user EnableOnSingleRunner add RunnerVisibilityControl before we process all nodes.
var existingEnableOnSingles = go.transform.GetComponentsInChildren<EnableOnSingleRunner>(false);
foreach (var enableOnSingleRunner in existingEnableOnSingles) {
enableOnSingleRunner.AddNodes();
}
RunnerVisibilityLink[] existingNodes = go.GetComponentsInChildren<RunnerVisibilityLink>(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<Component>(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<RunnerVisibilityLink>();
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<RunnerVisibilityLink>();
CommonObjectLookup.Add(guid, clones);
}
clones.Add(link);
}
/// <summary>
/// Reapplies a runner's IsVisibile setting to all of its registered visibility nodes.
/// </summary>
/// <param name="runner"></param>
/// <param name="refreshCommonObjects"></param>
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();
}
}
/// <summary>
/// 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.
/// </summary>
[StaticField]
private readonly static Dictionary<string, List<RunnerVisibilityLink>> CommonObjectLookup = new Dictionary<string, List<RunnerVisibilityLink>>();
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