|
|
#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 <color> 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
|