#if !FUSION_DEV #region Assets/Photon/Fusion/Editor/AssemblyAttributes/FusionEditorAssemblyAttributes.Common.cs // merged EditorAssemblyAttributes #region RegisterEditorLoader.cs // the default edit-mode loader [assembly:Fusion.Editor.FusionGlobalScriptableObjectEditorAttribute(typeof(Fusion.FusionGlobalScriptableObject), AllowEditMode = true, Order = int.MaxValue)] #endregion #endregion #region Assets/Photon/Fusion/Editor/AssetObjectEditor.cs namespace Fusion.Editor { using UnityEditor; [CustomEditor(typeof(AssetObject), true)] public class AssetObjectEditor : UnityEditor.Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); } } } #endregion #region Assets/Photon/Fusion/Editor/BehaviourEditor.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; [CustomEditor(typeof(Fusion.Behaviour), true)] [CanEditMultipleObjects] public partial class BehaviourEditor : FusionEditor { } } #endregion #region Assets/Photon/Fusion/Editor/ChildLookupEditor.cs // removed July 12 2021 #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/FixedBufferPropertyAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Fusion.Internal; using Unity.Collections.LowLevel.Unsafe; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; [CustomPropertyDrawer(typeof(FixedBufferPropertyAttribute))] unsafe class FixedBufferPropertyAttributeDrawer : PropertyDrawerWithErrorHandling { public const string FixedBufferFieldName = "Data"; public const string WrapperSurrogateDataPath = "Surrogate.Data"; private const float SpacingSubLabel = 2; private static readonly int _multiFieldPrefixId = "MultiFieldPrefixId".GetHashCode(); private static int[] _buffer = Array.Empty(); private static SurrogatePool _pool = new SurrogatePool(); private static GUIContent[] _vectorProperties = new[] { new GUIContent("X"), new GUIContent("Y"), new GUIContent("Z"), new GUIContent("W"), }; private Dictionary _needsSurrogateCache = new Dictionary(); private Dictionary _optimisedReaderWriters = new Dictionary(); private Type ActualFieldType => ((FixedBufferPropertyAttribute)attribute).Type; private int Capacity => ((FixedBufferPropertyAttribute)attribute).Capacity; private Type SurrogateType => ((FixedBufferPropertyAttribute)attribute).SurrogateType; public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { if (SurrogateType == null) { return EditorGUIUtility.singleLineHeight; } if (NeedsSurrogate(property)) { var fixedBufferProperty = GetFixedBufferProperty(property); var firstElement = fixedBufferProperty.GetFixedBufferElementAtIndex(0); if (!firstElement.IsArrayElement()) { // it seems that with multiple seclection child elements are not accessible Debug.Assert(property.serializedObject.targetObjects.Length > 1); return EditorGUIUtility.singleLineHeight; } var wrapper = _pool.Acquire(fieldInfo, Capacity, property, SurrogateType); try { return EditorGUI.GetPropertyHeight(wrapper.Property); } catch (Exception ex) { FusionEditorLog.ErrorInspector($"Error in GetPropertyHeight for {property.propertyPath}: {ex}"); return EditorGUIUtility.singleLineHeight; } } else { int count = 1; if (!EditorGUIUtility.wideMode) { count++; } return count * (EditorGUIUtility.singleLineHeight) + (count - 1) * EditorGUIUtility.standardVerticalSpacing; } } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { if (NeedsSurrogate(property)) { if (SurrogateType == null) { this.SetInfo($"[Networked] properties of type {ActualFieldType.FullName} in structs are not yet supported"); EditorGUI.LabelField(position, label, GUIContent.none); } else { int capacity = Capacity; var fixedBufferProperty = GetFixedBufferProperty(property); Array.Resize(ref _buffer, Math.Max(_buffer.Length, fixedBufferProperty.fixedBufferSize)); var firstElement = fixedBufferProperty.GetFixedBufferElementAtIndex(0); if (!firstElement.IsArrayElement()) { Debug.Assert(property.serializedObject.targetObjects.Length > 1); SetInfo($"Type does not support multi-edit"); EditorGUI.LabelField(position, label); } else { var wrapper = _pool.Acquire(fieldInfo, Capacity, property, SurrogateType); { bool surrogateOutdated = false; var targetObjects = property.serializedObject.targetObjects; if (targetObjects.Length > 1) { for (int i = 0; i < targetObjects.Length; ++i) { using (var so = new SerializedObject(targetObjects[i])) { using (var sp = so.FindPropertyOrThrow($"{property.propertyPath}.Data")) { if (UpdateSurrogateFromFixedBuffer(sp, wrapper.Surrogates[i], false, _pool.Flush)) { surrogateOutdated = true; } } } } if (surrogateOutdated) { // it seems that a mere Update won't do here wrapper.Property = new SerializedObject(wrapper.Wrappers).FindPropertyOrThrow(WrapperSurrogateDataPath); } } else { // an optimised path, no alloc needed Debug.Assert(wrapper.Surrogates.Length == 1); if (UpdateSurrogateFromFixedBuffer(fixedBufferProperty, wrapper.Surrogates[0], false, _pool.Flush)) { wrapper.Property.serializedObject.Update(); } } } // check if there has been any chagnes EditorGUI.BeginChangeCheck(); EditorGUI.BeginProperty(position, label, property); try { EditorGUI.PropertyField(position, wrapper.Property, label, true); } catch (Exception ex) { FusionEditorLog.ErrorInspector($"Error in OnGUIInternal for {property.propertyPath}: {ex}"); } EditorGUI.EndProperty(); if (EditorGUI.EndChangeCheck()) { wrapper.Property.serializedObject.ApplyModifiedProperties(); // when not having multiple different values, just write the whole thing if (UpdateSurrogateFromFixedBuffer(fixedBufferProperty, wrapper.Surrogates[0], true, !fixedBufferProperty.hasMultipleDifferentValues)) { fixedBufferProperty.serializedObject.ApplyModifiedProperties(); // refresh? wrapper.Property.serializedObject.Update(); } } } } } else { if (!_optimisedReaderWriters.TryGetValue(SurrogateType, out var surrogate)) { surrogate = (UnitySurrogateBase)Activator.CreateInstance(SurrogateType); _optimisedReaderWriters.Add(SurrogateType, surrogate); } if (ActualFieldType == typeof(float)) { DoFloatField(position, property, label, (IUnityValueSurrogate)surrogate); } else if (ActualFieldType == typeof(Vector2)) { DoFloatVectorProperty(position, property, label, 2, (IUnityValueSurrogate)surrogate); } else if (ActualFieldType == typeof(Vector3)) { DoFloatVectorProperty(position, property, label, 3, (IUnityValueSurrogate)surrogate); } else if (ActualFieldType == typeof(Vector4)) { DoFloatVectorProperty(position, property, label, 4, (IUnityValueSurrogate)surrogate); } } } private void DoFloatField(Rect position, SerializedProperty property, GUIContent label, IUnityValueSurrogate surrogate) { var fixedBuffer = GetFixedBufferProperty(property); Debug.Assert(1 == fixedBuffer.fixedBufferSize); var valueProp = fixedBuffer.GetFixedBufferElementAtIndex(0); int value = valueProp.intValue; surrogate.Read(&value, 1); EditorGUI.BeginProperty(position, label, property); EditorGUI.BeginChangeCheck(); surrogate.DataProperty = EditorGUI.FloatField(position, label, surrogate.DataProperty); if (EditorGUI.EndChangeCheck()) { surrogate.Write(&value, 1); valueProp.intValue = value; property.serializedObject.ApplyModifiedProperties(); } EditorGUI.EndProperty(); } private unsafe void DoFloatVectorProperty(Rect position, SerializedProperty property, GUIContent label, int count, IUnityValueSurrogate readerWriter) where T : unmanaged { EditorGUI.BeginProperty(position, label, property); try { var fixedBuffer = GetFixedBufferProperty(property); Debug.Assert(count == fixedBuffer.fixedBufferSize); int* raw = stackalloc int[count]; for (int i = 0; i < count; ++i) { raw[i] = fixedBuffer.GetFixedBufferElementAtIndex(i).intValue; } readerWriter.Read(raw, 1); int changed = 0; var data = readerWriter.DataProperty; float* pdata = (float*)&data; int id = GUIUtility.GetControlID(_multiFieldPrefixId, FocusType.Keyboard, position); position = UnityInternal.EditorGUI.MultiFieldPrefixLabel(position, id, label, count); if (position.width > 1) { using (new EditorGUI.IndentLevelScope(-EditorGUI.indentLevel)) { float w = (position.width - (count - 1) * SpacingSubLabel) / count; var nestedPosition = new Rect(position) { width = w }; for (int i = 0; i < count; ++i) { var propLabel = _vectorProperties[i]; float prefixWidth = EditorStyles.label.CalcSize(propLabel).x; using (new FusionEditorGUI.LabelWidthScope(prefixWidth)) { EditorGUI.BeginChangeCheck(); var newValue = propLabel == null ? EditorGUI.FloatField(nestedPosition, pdata[i]) : EditorGUI.FloatField(nestedPosition, propLabel, pdata[i]); if (EditorGUI.EndChangeCheck()) { changed |= (1 << i); pdata[i] = newValue; } } nestedPosition.x += w + SpacingSubLabel; } } } if (changed != 0) { readerWriter.DataProperty = data; readerWriter.Write(raw, 1); for (int i = 0; i < count; ++i) { if ((changed & (1 << i)) != 0) { fixedBuffer.GetFixedBufferElementAtIndex(i).intValue = raw[i]; } } property.serializedObject.ApplyModifiedProperties(); } } finally { EditorGUI.EndProperty(); } } private SerializedProperty GetFixedBufferProperty(SerializedProperty prop) { var result = prop.FindPropertyRelativeOrThrow(FixedBufferFieldName); Debug.Assert(result.isFixedBuffer); return result; } private bool NeedsSurrogate(SerializedProperty property) { if (_needsSurrogateCache.TryGetValue(property.propertyPath, out var result)) { return result; } result = true; if (ActualFieldType == typeof(float) || ActualFieldType == typeof(Vector2) || ActualFieldType == typeof(Vector3) || ActualFieldType == typeof(Vector4)) { var attributes = UnityInternal.ScriptAttributeUtility.GetFieldAttributes(fieldInfo); if (attributes == null || attributes.Count == 0) { // fast drawers do not support any additional attributes result = false; } } _needsSurrogateCache.Add(property.propertyPath, result); return result; } private bool UpdateSurrogateFromFixedBuffer(SerializedProperty sp, UnitySurrogateBase surrogate, bool write, bool force) { int count = sp.fixedBufferSize; Array.Resize(ref _buffer, Math.Max(_buffer.Length, count)); // need to get to the first property... `GetFixedBufferElementAtIndex` is slow and allocs var element = sp.Copy(); element.Next(true); // .Array element.Next(true); // .Array.size element.Next(true); // .Array.data[0] fixed (int* p = _buffer) { UnsafeUtility.MemClear(p, count * sizeof(int)); try { surrogate.Write(p, Capacity); } catch (Exception ex) { SetError($"Failed writing: {ex}"); } int i = 0; if (!force) { // find first difference for (; i < count; ++i, element.Next(true)) { Debug.Assert(element.propertyType == SerializedPropertyType.Integer); if (element.intValue != p[i]) { break; } } } if (i < count) { // update data if (write) { for (; i < count; ++i, element.Next(true)) { element.intValue = p[i]; } } else { for (; i < count; ++i, element.Next(true)) { p[i] = element.intValue; } } // update surrogate surrogate.Read(p, Capacity); return true; } else { return false; } } } private class SurrogatePool { private const int MaxTTL = 10; private FieldInfo _surrogateField = typeof(FusionUnitySurrogateBaseWrapper).GetField(nameof(FusionUnitySurrogateBaseWrapper.Surrogate)); private Dictionary<(Type, string, int), PropertyEntry> _used = new Dictionary<(Type, string, int), PropertyEntry>(); private Dictionary> _wrappersPool = new Dictionary>(); public SurrogatePool() { Undo.undoRedoPerformed += () => Flush = true; EditorApplication.update += () => { Flush = false; if (!WasUsed) { return; } WasUsed = false; var keysToRemove = new List<(Type, string, int)>(); foreach (var kv in _used) { var entry = kv.Value; if (--entry.TTL < 0) { // return to pool keysToRemove.Add(kv.Key); foreach (var wrapper in entry.Wrappers) { _wrappersPool[wrapper.Surrogate.GetType()].Push(wrapper); } } } // make all the wrappers available again foreach (var key in keysToRemove) { FusionEditorLog.TraceInspector($"Cleaning up {key}"); _used.Remove(key); } }; CompilationPipeline.compilationFinished += obj => { // destroy SO's, we don't want them to hold on to the surrogates var wrappers = _wrappersPool.Values.SelectMany(x => x) .Concat(_used.Values.SelectMany(x => x.Wrappers)); foreach (var wrapper in wrappers) { UnityEngine.Object.DestroyImmediate(wrapper); } }; } public bool Flush { get; private set; } public bool WasUsed { get; private set; } public PropertyEntry Acquire(FieldInfo field, int capacity, SerializedProperty property, Type type) { WasUsed = true; bool hadNulls = false; var key = (type, property.propertyPath, property.serializedObject.targetObjects.Length); if (_used.TryGetValue(key, out var entry)) { var countValid = entry.Wrappers.Count(x => x); if (countValid != entry.Wrappers.Length) { // something destroyed wrappers Debug.Assert(countValid == 0); _used.Remove(key); hadNulls = true; } else { entry.TTL = MaxTTL; return entry; } } // acquire new entry var wrappers = new FusionUnitySurrogateBaseWrapper[key.Item3]; if (!_wrappersPool.TryGetValue(type, out var pool)) { pool = new Stack(); _wrappersPool.Add(type, pool); } for (int i = 0; i < wrappers.Length; ++i) { // pop destroyed ones while (pool.Count > 0 && !pool.Peek()) { pool.Pop(); hadNulls = true; } if (pool.Count > 0) { wrappers[i] = pool.Pop(); } else { FusionEditorLog.TraceInspector($"Allocating surrogate {type}"); wrappers[i] = ScriptableObject.CreateInstance(); } if (wrappers[i].SurrogateType != type) { FusionEditorLog.TraceInspector($"Replacing type {wrappers[i].Surrogate?.GetType()} with {type}"); wrappers[i].Surrogate = (UnitySurrogateBase)Activator.CreateInstance(type); wrappers[i].Surrogate.Init(capacity); wrappers[i].SurrogateType = type; } } FusionEditorLog.TraceInspector($"Created entry for {property.propertyPath}"); entry = new PropertyEntry() { Property = new SerializedObject(wrappers).FindPropertyOrThrow(WrapperSurrogateDataPath), Surrogates = wrappers.Select(x => x.Surrogate).ToArray(), TTL = MaxTTL, Wrappers = wrappers }; _used.Add(key, entry); if (hadNulls) { GUIUtility.ExitGUI(); } return entry; } public class PropertyEntry { public SerializedProperty Property; public UnitySurrogateBase[] Surrogates; public int TTL; public FusionUnitySurrogateBaseWrapper[] Wrappers; } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/INetworkPrefabSourceDrawer.cs namespace Fusion.Editor { using System; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(INetworkPrefabSource), true)] class INetworkPrefabSourceDrawer : PropertyDrawerWithErrorHandling { const int ThumbnailWidth = 20; protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { EditorGUI.BeginChangeCheck(); var source = property.managedReferenceValue as INetworkPrefabSource; position = DrawThumbnailPrefix(position, source); source = DrawSourceObjectPicker(position, GUIContent.none, source); if (EditorGUI.EndChangeCheck()) { // see how it can be loaded property.managedReferenceValue = source; property.serializedObject.ApplyModifiedProperties(); } } } public static Rect DrawThumbnailPrefix(Rect position, INetworkPrefabSource source) { if (source == null) { return position; } var pos = position; pos.width = ThumbnailWidth; FusionEditorGUI.DrawTypeThumbnail(pos, source.GetType(), "NetworkPrefabSource", source.Description); position.xMin += ThumbnailWidth; return position; } public static void DrawThumbnail(Rect position, INetworkPrefabSource source) { if (source == null) { return; } var pos = position; pos.x += (pos.width - ThumbnailWidth) / 2; pos.width = ThumbnailWidth; FusionEditorGUI.DrawTypeThumbnail(pos, source.GetType(), "NetworkPrefabSource", source.Description); } public static INetworkPrefabSource DrawSourceObjectPicker(Rect position, GUIContent label, INetworkPrefabSource source) { NetworkProjectConfigUtilities.TryGetPrefabEditorInstance(source?.AssetGuid ?? default, out var target); EditorGUI.BeginChangeCheck(); target = NetworkPrefabRefDrawer.DrawNetworkPrefabPicker(position, label, target); if (EditorGUI.EndChangeCheck()) { if (target) { var factory = new NetworkAssetSourceFactory(); return factory.TryCreatePrefabSource(new NetworkAssetSourceFactoryContext(target)); } else { return null; } } else { return source; } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/NetworkBoolDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(NetworkBool))] public class NetworkBoolDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { using (new FusionEditorGUI.PropertyScope(position, label, property)) { var valueProperty = property.FindPropertyRelativeOrThrow("_value"); EditorGUI.BeginChangeCheck(); bool isChecked = EditorGUI.Toggle(position, label, valueProperty.intValue > 0); if (EditorGUI.EndChangeCheck()) { valueProperty.intValue = isChecked ? 1 : 0; valueProperty.serializedObject.ApplyModifiedProperties(); } } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/NetworkObjectGuidDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(NetworkObjectGuid))] [FusionPropertyDrawerMeta(HasFoldout = false)] class NetworkObjectGuidDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var guid = GetValue(property); using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { if (!GUI.enabled) { GUI.enabled = true; EditorGUI.SelectableLabel(position, $"{(System.Guid)guid}"); GUI.enabled = false; } else { EditorGUI.BeginChangeCheck(); var text = EditorGUI.TextField(position, ((System.Guid)guid).ToString()); ClearErrorIfLostFocus(); if (EditorGUI.EndChangeCheck()) { if (NetworkObjectGuid.TryParse(text, out guid)) { SetValue(property, guid); property.serializedObject.ApplyModifiedProperties(); } else { SetError($"Unable to parse {text}"); } } } } } public static unsafe NetworkObjectGuid GetValue(SerializedProperty property) { var guid = new NetworkObjectGuid(); var prop = property.FindPropertyRelativeOrThrow(nameof(NetworkObjectGuid.RawGuidValue)); guid.RawGuidValue[0] = prop.GetFixedBufferElementAtIndex(0).longValue; guid.RawGuidValue[1] = prop.GetFixedBufferElementAtIndex(1).longValue; return guid; } public static unsafe void SetValue(SerializedProperty property, NetworkObjectGuid guid) { var prop = property.FindPropertyRelativeOrThrow(nameof(NetworkObjectGuid.RawGuidValue)); prop.GetFixedBufferElementAtIndex(0).longValue = guid.RawGuidValue[0]; prop.GetFixedBufferElementAtIndex(1).longValue = guid.RawGuidValue[1]; } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/NetworkPrefabAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(NetworkPrefabAttribute))] [FusionPropertyDrawerMeta(HasFoldout = false)] class NetworkPrefabAttributeDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var leafType = fieldInfo.FieldType.GetUnityLeafType(); if (leafType != typeof(GameObject) && leafType != typeof(NetworkObject) && !leafType.IsSubclassOf(typeof(NetworkObject))) { SetError($"{nameof(NetworkPrefabAttribute)} only works for {typeof(GameObject)} and {typeof(NetworkObject)} fields"); return; } using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { GameObject prefab; if (leafType == typeof(GameObject)) { prefab = (GameObject)property.objectReferenceValue; } else { var component = (NetworkObject)property.objectReferenceValue; prefab = component != null ? component.gameObject : null; } EditorGUI.BeginChangeCheck(); prefab = (GameObject)EditorGUI.ObjectField(position, prefab, typeof(GameObject), false); // ensure the results are filtered if (UnityInternal.ObjectSelector.isVisible) { var selector = UnityInternal.ObjectSelector.get; if (UnityInternal.EditorGUIUtility.LastControlID == selector.objectSelectorID) { var filter = selector.searchFilter; if (!filter.Contains(NetworkProjectConfigImporter.FusionPrefabTagSearchTerm)) { if (string.IsNullOrEmpty(filter)) { filter = NetworkProjectConfigImporter.FusionPrefabTagSearchTerm; } else { filter = NetworkProjectConfigImporter.FusionPrefabTagSearchTerm + " " + filter; } selector.searchFilter = filter; } } } if (EditorGUI.EndChangeCheck()) { UnityEngine.Object result; if (!prefab) { result = null; } else { if (leafType == typeof(GameObject)) { result = prefab; } else { result = prefab.GetComponent(leafType); if (!result) { SetError($"Prefab {prefab} does not have a {leafType} component"); return; } } } property.objectReferenceValue = prefab; property.serializedObject.ApplyModifiedProperties(); } if (prefab) { var no = prefab.GetComponent(); if (!no) { SetError($"Prefab {prefab} does not have a {nameof(NetworkObject)} component"); } if (!AssetDatabaseUtils.HasLabel(prefab, NetworkProjectConfigImporter.FusionPrefabTag)) { SetError($"Prefab {prefab} is not tagged as a Fusion prefab. Try reimporting."); } } } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/NetworkPrefabRefDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(NetworkPrefabRef))] [FusionPropertyDrawerMeta(HasFoldout = false)] class NetworkPrefabRefDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var prefabRef = NetworkObjectGuidDrawer.GetValue(property); using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { NetworkObject prefab = null; if (prefabRef.IsValid && !NetworkProjectConfigUtilities.TryGetPrefabEditorInstance(prefabRef, out prefab)) { SetError($"Prefab with guid {prefabRef} not found."); } EditorGUI.BeginChangeCheck(); prefab = DrawNetworkPrefabPicker(position, GUIContent.none, prefab); if (EditorGUI.EndChangeCheck()) { if (prefab) { prefabRef = NetworkObjectEditor.GetPrefabGuid(prefab); } else { prefabRef = default; } NetworkObjectGuidDrawer.SetValue(property, prefabRef); property.serializedObject.ApplyModifiedProperties(); } SetInfo($"{prefabRef}"); if (prefab) { var expectedPrefabRef = NetworkObjectEditor.GetPrefabGuid(prefab); if (!prefabRef.Equals(expectedPrefabRef)) { SetError($"Resolved {prefab} has a different guid ({expectedPrefabRef}) than expected ({prefabRef}). " + $"This can happen if prefabs are incorrectly resolved, e.g. when there are multiple resources of the same name."); } else if (!expectedPrefabRef.IsValid) { SetError($"Prefab {prefab} needs to be reimported."); } else if (!AssetDatabaseUtils.HasLabel(prefab, NetworkProjectConfigImporter.FusionPrefabTag)) { SetError($"Prefab {prefab} is not tagged as a Fusion prefab. Try reimporting."); } else { // ClearError(); } } } } public static NetworkObject DrawNetworkPrefabPicker(Rect position, GUIContent label, NetworkObject prefab) { var prefabGo = (GameObject)EditorGUI.ObjectField(position, label, prefab ? prefab.gameObject : null, typeof(GameObject), false); // ensure the results are filtered if (UnityInternal.ObjectSelector.isVisible) { var selector = UnityInternal.ObjectSelector.get; if (UnityInternal.EditorGUIUtility.LastControlID == selector.objectSelectorID) { var filter = selector.searchFilter; if (!filter.Contains(NetworkProjectConfigImporter.FusionPrefabTagSearchTerm)) { if (string.IsNullOrEmpty(filter)) { filter = NetworkProjectConfigImporter.FusionPrefabTagSearchTerm; } else { filter = NetworkProjectConfigImporter.FusionPrefabTagSearchTerm + " " + filter; } selector.searchFilter = filter; } } } if (prefabGo) { return prefabGo.GetComponent(); } else { return null; } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/NetworkStringDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(NetworkString<>))] [FusionPropertyDrawerMeta(HasFoldout = false)] class NetworkStringDrawer : PropertyDrawerWithErrorHandling { private string _str = ""; private Action _write; private Action _read; private int _expectedLength; public NetworkStringDrawer() { _write = (buffer, count) => { unsafe { fixed (int* p = buffer) { _str = new string((sbyte*)p, 0, Mathf.Clamp(_expectedLength, 0, count) * 4, Encoding.UTF32); } } }; _read = (buffer, count) => { unsafe { fixed (int* p = buffer) { var charCount = UTF32Tools.Convert(_str, (uint*)p, count).CharacterCount; if (charCount < _str.Length) { _str = _str.Substring(0, charCount); } } } }; } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var length = property.FindPropertyRelativeOrThrow(nameof(NetworkString<_2>._length)); var data = property.FindPropertyRelativeOrThrow($"{nameof(NetworkString<_2>._data)}.Data"); _expectedLength = length.intValue; data.UpdateFixedBuffer(_read, _write, false); EditorGUI.BeginChangeCheck(); using (new FusionEditorGUI.ShowMixedValueScope(data.hasMultipleDifferentValues)) { _str = EditorGUI.TextField(position, label, _str); } if (EditorGUI.EndChangeCheck()) { _expectedLength = _str.Length; if (data.UpdateFixedBuffer(_read, _write, true, data.hasMultipleDifferentValues)) { length.intValue = Encoding.UTF32.GetByteCount(_str) / 4; data.serializedObject.ApplyModifiedProperties(); } } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/NormalizedRectAttributeDrawer.cs namespace Fusion.Editor { using System; using UnityEditor; using UnityEngine; #if UNITY_EDITOR [CustomPropertyDrawer(typeof(NormalizedRectAttribute))] public class NormalizedRectAttributeDrawer : PropertyDrawer { bool isDragNewRect; bool isDragXMin, isDragXMax, isDragYMin, isDragYMax, isDragAll; MouseCursor lockCursorStyle; Vector2 mouseDownStart; static GUIStyle _compactLabelStyle; static GUIStyle _compactValueStyle; const float EXPANDED_HEIGHT = 140; const float COLLAPSE_HEIGHT = 48; public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { if (property.propertyType == SerializedPropertyType.Rect) { return property.isExpanded ? EXPANDED_HEIGHT : COLLAPSE_HEIGHT; } else { return base.GetPropertyHeight(property, label); } } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); bool hasChanged = false; EditorGUI.LabelField(new Rect(position) { height = 17 }, label); var value = property.rectValue; if (property.propertyType == SerializedPropertyType.Rect) { var dragarea = new Rect(position) { yMin = position.yMin + 16 + 3, yMax = position.yMax - 2, //xMin = position.xMin + 16, //xMax = position.xMax - 4 }; // lower foldout box GUI.Box(dragarea, GUIContent.none, EditorStyles.helpBox); property.isExpanded = GUI.Toggle(new Rect(position) { xMin = dragarea.xMin + 2, yMin = dragarea.yMin + 2, width = 12, height = 16 }, property.isExpanded, GUIContent.none, EditorStyles.foldout); bool isExpanded = property.isExpanded; float border = isExpanded ? 4 : 2; dragarea.xMin += 18; dragarea.yMin += border; dragarea.xMax -= border; dragarea.yMax -= border; // Reshape the inner box to the correct aspect ratio if (isExpanded) { var ratio = (attribute as NormalizedRectAttribute).AspectRatio; if (ratio == 0) { var currentRes = UnityEditor.Handles.GetMainGameViewSize(); ratio = currentRes.x / currentRes.y; } // Don't go any wider than the inspector box. var width = (dragarea.height * ratio); if (width < dragarea.width) { var x = (dragarea.width - width) / 2; dragarea.x = dragarea.xMin + (int)x; dragarea.width = (int)(width); } } // Simulated desktop rect GUI.Box(dragarea, GUIContent.none, EditorStyles.helpBox); var invertY = (attribute as NormalizedRectAttribute).InvertY; Event e = Event.current; const int HANDLE_SIZE = 8; var normmin = new Vector2(value.xMin, invertY ? 1f - value.yMin : value.yMin); var normmax = new Vector2(value.xMax, invertY ? 1f - value.yMax : value.yMax); var minreal = Rect.NormalizedToPoint(dragarea, normmin); var maxreal = Rect.NormalizedToPoint(dragarea, normmax); var lowerleftrect = new Rect(minreal.x , minreal.y - (invertY ? HANDLE_SIZE : 0), HANDLE_SIZE, HANDLE_SIZE); var upperrghtrect = new Rect(maxreal.x - HANDLE_SIZE, maxreal.y - (invertY ? 0 : HANDLE_SIZE), HANDLE_SIZE, HANDLE_SIZE); var upperleftrect = new Rect(minreal.x , maxreal.y - (invertY ? 0 : HANDLE_SIZE), HANDLE_SIZE, HANDLE_SIZE); var lowerrghtrect = new Rect(maxreal.x - HANDLE_SIZE, minreal.y - (invertY ? HANDLE_SIZE : 0), HANDLE_SIZE, HANDLE_SIZE); var currentrect = Rect.MinMaxRect(minreal.x, invertY ? maxreal.y : minreal.y, maxreal.x, invertY ? minreal.y : maxreal.y); if (lockCursorStyle == MouseCursor.Arrow) { if (isExpanded) { EditorGUIUtility.AddCursorRect(lowerleftrect, MouseCursor.Link); EditorGUIUtility.AddCursorRect(upperrghtrect, MouseCursor.Link); EditorGUIUtility.AddCursorRect(upperleftrect, MouseCursor.Link); EditorGUIUtility.AddCursorRect(lowerrghtrect, MouseCursor.Link); } EditorGUIUtility.AddCursorRect(currentrect, MouseCursor.MoveArrow); } else { // Lock cursor to a style while dragging, otherwise the slow inspector update causes rapid mouse icon changes. EditorGUIUtility.AddCursorRect(dragarea, lockCursorStyle); } EditorGUI.DrawRect(lowerleftrect, Color.yellow); EditorGUI.DrawRect(upperrghtrect, Color.yellow); EditorGUI.DrawRect(upperleftrect, Color.yellow); EditorGUI.DrawRect(lowerrghtrect, Color.yellow); var mousepos = e.mousePosition; if (e.button == 0) { if (e.type == EventType.MouseUp) { isDragXMin = false; isDragYMin = false; isDragXMax = false; isDragYMax = false; isDragAll = false; lockCursorStyle = MouseCursor.Arrow; isDragNewRect = false; hasChanged = true; } if (e.type == EventType.MouseDown ) { if (isExpanded && lowerleftrect.Contains(mousepos)) { isDragXMin = true; isDragYMin = true; lockCursorStyle = MouseCursor.Link; } else if (isExpanded && upperrghtrect.Contains(mousepos)) { isDragXMax = true; isDragYMax = true; lockCursorStyle = MouseCursor.Link; } else if (isExpanded && upperleftrect.Contains(mousepos)) { isDragXMin = true; isDragYMax = true; lockCursorStyle = MouseCursor.Link; } else if (isExpanded && lowerrghtrect.Contains(mousepos)) { isDragXMax = true; isDragYMin = true; lockCursorStyle = MouseCursor.Link; } else if (currentrect.Contains(mousepos)) { isDragAll = true; // mouse start is stored as a normalized offset from the Min values. mouseDownStart = Rect.PointToNormalized(dragarea, mousepos) - normmin; lockCursorStyle = MouseCursor.MoveArrow; } else if (isExpanded && dragarea.Contains(mousepos)) { mouseDownStart = mousepos; isDragNewRect = true; } } } if (e.type == EventType.MouseDrag) { Rect rect; if (isDragNewRect) { var start = Rect.PointToNormalized(dragarea, mouseDownStart); var end = Rect.PointToNormalized(dragarea, e.mousePosition); if (invertY) { rect = Rect.MinMaxRect( Math.Max(0f, Math.Min(start.x, end.x)), Math.Max(0f, 1f - Math.Max(start.y, end.y)), Math.Min(1f, Math.Max(start.x, end.x)), Math.Min(1f, 1f - Math.Min(start.y, end.y)) ); } else { rect = Rect.MinMaxRect( Math.Max(0f, Math.Min(start.x, end.x)), Math.Max(0f, Math.Min(start.y, end.y)), Math.Min(1f, Math.Max(start.x, end.x)), Math.Min(1f, Math.Max(start.y, end.y)) ); } property.rectValue = rect; hasChanged = true; } else if (isDragAll){ var normmouse = Rect.PointToNormalized(dragarea, e.mousePosition); rect = new Rect(value) { x = Math.Max(normmouse.x - mouseDownStart.x, 0), y = Math.Max(invertY ? (1 - normmouse.y + mouseDownStart.y) : (normmouse.y - mouseDownStart.y), 0) }; if (rect.xMax > 1) { rect = new Rect(rect) { x = rect.x + (1f - rect.xMax)}; } if (rect.yMax > 1) { rect = new Rect(rect) { y = rect.y + (1f - rect.yMax) }; } property.rectValue = rect; hasChanged = true; } else if (isDragXMin || isDragXMax || isDragYMin || isDragYMax) { const float VERT_HANDLE_MIN_DIST = .2f; const float HORZ_HANDLE_MIN_DIST = .05f; var normmouse = Rect.PointToNormalized(dragarea, e.mousePosition); if (invertY) { rect = Rect.MinMaxRect( isDragXMin ? Math.Min( normmouse.x, value.xMax - HORZ_HANDLE_MIN_DIST) : value.xMin, isDragYMin ? Math.Min(1f - normmouse.y, value.yMax - VERT_HANDLE_MIN_DIST) : value.yMin, isDragXMax ? Math.Max( normmouse.x, value.xMin + HORZ_HANDLE_MIN_DIST) : value.xMax, isDragYMax ? Math.Max(1f - normmouse.y, value.yMin + VERT_HANDLE_MIN_DIST) : value.yMax ); } else { rect = Rect.MinMaxRect( isDragXMin ? Math.Min(normmouse.x, value.xMax - HORZ_HANDLE_MIN_DIST) : value.xMin, isDragYMin ? Math.Min(normmouse.y, value.yMax - VERT_HANDLE_MIN_DIST) : value.yMin, isDragXMax ? Math.Max(normmouse.x, value.xMin + HORZ_HANDLE_MIN_DIST) : value.xMax, isDragYMax ? Math.Max(normmouse.y, value.yMin + VERT_HANDLE_MIN_DIST) : value.yMax ); } property.rectValue = rect; hasChanged = true; } } const float SPACING = 4f; const int LABELS_WIDTH = 16; const float COMPACT_THRESHOLD = 340f; bool useCompact = position.width < COMPACT_THRESHOLD; var labelwidth = EditorGUIUtility.labelWidth; var fieldwidth = (position.width - labelwidth- 3 * SPACING) * 0.25f ; var fieldbase = new Rect(position) { xMin = position.xMin + labelwidth, height = 16, width = fieldwidth - (useCompact ? 0 : LABELS_WIDTH) }; if (_compactValueStyle == null) { _compactLabelStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 9, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(2, 0, 1, 0) }; _compactValueStyle = new GUIStyle(EditorStyles.miniTextField) { fontSize = 9, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(2, 0, 1, 0) }; } GUIStyle valueStyle = _compactValueStyle; //if (useCompact) { // if (_compactStyle == null) { // _compactStyle = new GUIStyle(EditorStyles.miniTextField) { fontSize = 9, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(2, 0, 1, 0) }; // } // valueStyle = _compactStyle; //} else { // valueStyle = EditorStyles.textField; //} // Only draw labels when not in compact if (!useCompact) { Rect l1 = new Rect(fieldbase) { x = fieldbase.xMin }; Rect l2 = new Rect(fieldbase) { x = fieldbase.xMin + 1 * (fieldwidth + SPACING) }; Rect l3 = new Rect(fieldbase) { x = fieldbase.xMin + 2 * (fieldwidth + SPACING) }; Rect l4 = new Rect(fieldbase) { x = fieldbase.xMin + 3 * (fieldwidth + SPACING) }; GUI.Label(l1, "L:", _compactLabelStyle); GUI.Label(l2, "R:", _compactLabelStyle); GUI.Label(l3, "T:", _compactLabelStyle); GUI.Label(l4, "B:", _compactLabelStyle); } // Draw value fields Rect f1 = new Rect(fieldbase) { x = fieldbase.xMin + 0 * fieldwidth + (useCompact ? 0 : LABELS_WIDTH) }; Rect f2 = new Rect(fieldbase) { x = fieldbase.xMin + 1 * fieldwidth + (useCompact ? 0 : LABELS_WIDTH) + 1 * SPACING }; Rect f3 = new Rect(fieldbase) { x = fieldbase.xMin + 2 * fieldwidth + (useCompact ? 0 : LABELS_WIDTH) + 2 * SPACING }; Rect f4 = new Rect(fieldbase) { x = fieldbase.xMin + 3 * fieldwidth + (useCompact ? 0 : LABELS_WIDTH) + 3 * SPACING }; using (var check = new EditorGUI.ChangeCheckScope()) { float newxmin, newxmax, newymin, newymax; if (invertY) { newxmin = EditorGUI.DelayedFloatField(f1, (float)Math.Round(value.xMin, useCompact ? 2 : 3), valueStyle); newxmax = EditorGUI.DelayedFloatField(f2, (float)Math.Round(value.xMax, useCompact ? 2 : 3), valueStyle); newymax = EditorGUI.DelayedFloatField(f3, (float)Math.Round(value.yMax, useCompact ? 2 : 3), valueStyle); newymin = EditorGUI.DelayedFloatField(f4, (float)Math.Round(value.yMin, useCompact ? 2 : 3), valueStyle); } else { newxmin = EditorGUI.DelayedFloatField(f1, (float)Math.Round(value.xMin, useCompact ? 2 : 3), valueStyle); newxmax = EditorGUI.DelayedFloatField(f2, (float)Math.Round(value.xMax, useCompact ? 2 : 3), valueStyle); newymin = EditorGUI.DelayedFloatField(f3, (float)Math.Round(value.yMin, useCompact ? 2 : 3), valueStyle); newymax = EditorGUI.DelayedFloatField(f4, (float)Math.Round(value.yMax, useCompact ? 2 : 3), valueStyle); } if (check.changed) { if (newxmin != value.xMin) value.xMin = Math.Min(newxmin, value.xMax - .05f); if (newxmax != value.xMax) value.xMax = Math.Max(newxmax, value.xMin + .05f); if (newymax != value.yMax) value.yMax = Math.Max(newymax, value.yMin + .05f); if (newymin != value.yMin) value.yMin = Math.Min(newymin, value.yMax - .05f); property.rectValue = value; property.serializedObject.ApplyModifiedProperties(); } } var nmins = new Vector2(value.xMin, invertY ? 1f - value.yMin : value.yMin); var nmaxs = new Vector2(value.xMax, invertY ? 1f - value.yMax : value.yMax); var mins = Rect.NormalizedToPoint(dragarea, nmins); var maxs = Rect.NormalizedToPoint(dragarea, nmaxs); var area = Rect.MinMaxRect(minreal.x, invertY ? maxreal.y : minreal.y, maxreal.x, invertY ? minreal.y : maxreal.y); EditorGUI.DrawRect(area, new Color(1f, 1f, 1f, .1f)); //GUI.DrawTexture(area, GUIContent.none, EditorStyles.helpBox); //GUI.Box(area, GUIContent.none, EditorStyles.helpBox); } else { Debug.LogWarning($"{nameof(NormalizedRectAttribute)} only valid on UnityEngine.Rect fields. Will use default rendering for '{property.type} {property.name}' in class '{fieldInfo.DeclaringType}'."); EditorGUI.PropertyField(position, property, label); } if (hasChanged) { GUI.changed = true; property.serializedObject.ApplyModifiedProperties(); } EditorGUI.EndProperty(); } } #endif } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/SceneRefDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(SceneRef))] public class SceneRefDrawer : PropertyDrawer { public const int CheckboxWidth = 16; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { var valueProperty = property.FindPropertyRelativeOrThrow(nameof(SceneRef.RawValue)); long rawValue = valueProperty.longValue; var togglePos = position; togglePos.width = CheckboxWidth; bool hasValue = rawValue > 0; EditorGUI.BeginChangeCheck(); if (EditorGUI.Toggle(togglePos, hasValue) != hasValue) { rawValue = valueProperty.longValue = hasValue ? 0 : 1; valueProperty.serializedObject.ApplyModifiedProperties(); } if (rawValue > 0) { position.xMin += togglePos.width; rawValue = EditorGUI.LongField(position, rawValue - 1); rawValue = Math.Max(0, rawValue) + 1; if (EditorGUI.EndChangeCheck()) { valueProperty.longValue = rawValue; valueProperty.serializedObject.ApplyModifiedProperties(); } } } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/SerializableDictionaryDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEditorInternal; using UnityEngine; [CustomPropertyDrawer(typeof(SerializableDictionary), true)] class SerializableDictionaryDrawer : PropertyDrawerWithErrorHandling { const string ItemsPropertyPath = SerializableDictionary.ItemsPropertyPath; const string EntryKeyPropertyPath = SerializableDictionary.EntryKeyPropertyPath; protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var entries = property.FindPropertyRelativeOrThrow(ItemsPropertyPath); entries.isExpanded = property.isExpanded; using (new FusionEditorGUI.PropertyScope(position, label, property)) { EditorGUI.PropertyField(position, entries, label, true); property.isExpanded = entries.isExpanded; string error = VerifyDictionary(entries, EntryKeyPropertyPath); if (error != null) { SetError(error); } else { ClearError(); } } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { var entries = property.FindPropertyRelativeOrThrow(ItemsPropertyPath); return EditorGUI.GetPropertyHeight(entries, label, true); } private static HashSet _dictionaryKeyHash = new HashSet(new SerializedPropertyUtilities.SerializedPropertyEqualityComparer()); private static string VerifyDictionary(SerializedProperty prop, string keyPropertyName) { Debug.Assert(prop.isArray); try { for (int i = 0; i < prop.arraySize; ++i) { var keyProperty = prop.GetArrayElementAtIndex(i).FindPropertyRelativeOrThrow(keyPropertyName); if (!_dictionaryKeyHash.Add(keyProperty)) { var groups = Enumerable.Range(0, prop.arraySize) .GroupBy(x => prop.GetArrayElementAtIndex(x).FindPropertyRelative(keyPropertyName), x => x, _dictionaryKeyHash.Comparer) .Where(x => x.Count() > 1) .ToList(); // there are duplicates - take the slow and allocating path now return string.Join("\n", groups.Select(x => $"Duplicate keys for elements: {string.Join(", ", x)}")); } } return null; } finally { _dictionaryKeyHash.Clear(); } } } } #endregion #region Assets/Photon/Fusion/Editor/CustomTypes/TickRateDrawer.cs namespace Fusion.Editor { using System; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(TickRate.Selection))] [FusionPropertyDrawerMeta(HasFoldout = false)] public class TickRateDrawer : PropertyDrawer { private const int PAD = 0; // Cached pop items for client rate private static GUIContent[] _clientRateOptions; private static GUIContent[] ClientRateOptions { get { if (_clientRateOptions != null) { return _clientRateOptions; } ExtractClientRates(); return _clientRateOptions; } } // Cached pop items for client rate private static int[] _clientRateValues; private static int[] ClientRateValues { get { if (_clientRateValues != null) { return _clientRateValues; } ExtractClientRates(); return _clientRateValues; } } // // private static GUIContent[] _ratioOptions = new GUIContent[4]; // private static int[] _ratioValues = new int[] { 0, 1, 2, 3 }; private static readonly GUIContent[][] _reusableRatioGUIArrays = new GUIContent[4][] { new GUIContent[1], new GUIContent[2], new GUIContent[3], new GUIContent[4] }; private static readonly int[][] _reusableRatioIntArrays = new int[4][] { new int[1], new int[2], new int[3], new int[4] }; private static readonly GUIContent[][] _reusableServerGUIArrays = new GUIContent[4][] { new GUIContent[1], new GUIContent[2], new GUIContent[3], new GUIContent[4] }; private static readonly int[][] _reusableServerIntArrays = new int[4][] { new int[1], new int[2], new int[3], new int[4] }; private static readonly LazyGUIStyle _buttonStyle = LazyGUIStyle.Create(_ => new GUIStyle(EditorStyles.miniButton) { fontSize = 9, alignment = TextAnchor.MiddleCenter }); public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return (base.GetPropertyHeight(property, label) + EditorGUIUtility.standardVerticalSpacing) * 4 + PAD * 2; } private int DrawPopup(ref Rect labelRect, ref Rect fieldRect, float rowHeight, GUIContent guiContent, int[] sliderValues, int[] values, GUIContent[] options, int currentValue, int offset = 0) { EditorGUI.LabelField(labelRect, guiContent); int indentHold = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; int value; Rect dropRect = fieldRect; if (fieldRect.width > 120) { var slideRect = new Rect(fieldRect) { xMax = fieldRect.xMax - 64 }; dropRect = new Rect(fieldRect) { xMin = fieldRect.xMax - 64 }; var sliderRange = Math.Max(3, sliderValues.Length - 1); if (sliderRange == 3) { var dividerRect = new Rect(slideRect); // dividerRect.yMin += 2; // dividerRect.yMax -= 2; var quarter = slideRect.width * 1f /4; using (new EditorGUI.DisabledScope(!(options.Length + offset >= 4))) { if (GUI.Toggle(new Rect(dividerRect) { width = quarter }, currentValue == 3, new GUIContent("1/8"), _buttonStyle )) { currentValue = 3; } } using (new EditorGUI.DisabledScope(!(options.Length + offset >= 3 && offset <= 2))) { if (GUI.Toggle(new Rect(dividerRect) { width = quarter, x = dividerRect.x + quarter }, currentValue == 2, new GUIContent("1/4"), _buttonStyle)) { currentValue = 2; } } using (new EditorGUI.DisabledScope(!(options.Length + offset >= 2 && offset <= 1))) { if (GUI.Toggle(new Rect(dividerRect) { width = quarter, x = dividerRect.x + quarter * 2 }, currentValue == 1, new GUIContent("1/2"), _buttonStyle)) { currentValue = 1; } } using (new EditorGUI.DisabledScope(!(options.Length + offset >= 1 && offset == 0))) { if (GUI.Toggle(new Rect(dividerRect) { width = quarter, x = dividerRect.x + quarter * 3 }, currentValue == 0, new GUIContent("1:1"), _buttonStyle)) { currentValue = 0; } } EditorGUI.LabelField(dropRect, options[currentValue - offset], new GUIStyle(EditorStyles.label){padding = new RectOffset(4, 0, 0, 0)}); value = values[currentValue - offset]; } else { currentValue = (int)GUI.HorizontalSlider(slideRect, (float)currentValue, sliderRange, 0); // Clamp slider ranges into valid enum ranges if (currentValue - offset < 0) { currentValue = offset; } else if (currentValue - offset >= values.Length) { currentValue = values.Length - 1 + offset; } value = values[EditorGUI.Popup(dropRect, GUIContent.none, currentValue - offset, options)]; // value = values[EditorGUI.Popup(fieldRect, GUIContent.none, currentValue - offset, options)]; } } else { // Handling for very narrow window. Falls back to just a basic popup for each value. dropRect = fieldRect; value = values[EditorGUI.Popup(dropRect, GUIContent.none, currentValue - offset, options)]; } EditorGUI.indentLevel = indentHold; labelRect.y += rowHeight + EditorGUIUtility.standardVerticalSpacing; fieldRect.y += rowHeight + EditorGUIUtility.standardVerticalSpacing; return value; } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { using (new FusionEditorGUI.PropertyScope(position, label, property)) { var rowHeight = base.GetPropertyHeight(property, label); // using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { // EditorGUI.LabelField(new Rect(position){ yMin = position.yMin + rowHeight}, GUIContent.none, FusionGUIStyles.GroupBoxType.Gray.GetStyle()); position = new Rect(position) { xMin = position.xMin + PAD, xMax = position.xMax - PAD, yMin = position.yMin + PAD, yMax = position.yMax - PAD }; var clientRateProperty = property.FindPropertyRelativeOrThrow(nameof(TickRate.Selection.Client)); var serverRateProperty = property.FindPropertyRelativeOrThrow(nameof(TickRate.Selection.ServerIndex)); var clientSendProperty = property.FindPropertyRelativeOrThrow(nameof(TickRate.Selection.ClientSendIndex)); var serverSendProperty = property.FindPropertyRelativeOrThrow(nameof(TickRate.Selection.ServerSendIndex)); var selection = GetSelectionValue(property); var hold = selection; var tickRate = TickRate.Get(TickRate.IsValid(selection.Client) ? selection.Client : TickRate.Default.Client); var clientRateIndex = GetIndexForClientRate(tickRate.Client); var rect = new Rect(position) { height = base.GetPropertyHeight(property, label) }; //var fieldWidth = Math.Max(Math.Min(position.width * .33f, MAX_FIELD_WIDTH), MIN_FIELD_WIDTH); var labelWidth = EditorGUIUtility.labelWidth; var labelRect = new Rect(rect) { width = labelWidth}; // { xMax = rect.xMax - fieldWidth }}; //var fieldRect = new Rect(rect) { xMin = rect.xMax -fieldWidth }; var fieldRect = new Rect(rect) { xMin = rect.xMin + labelWidth}; // CLIENT SIM RATE selection.Client = DrawPopup(ref labelRect, ref fieldRect, rowHeight, new GUIContent("Client Tick Rate"), _clientRateValues,_clientRateValues, ClientRateOptions, clientRateIndex); // TODO: This validates every tick without checking for changes. May be good, may not. selection = tickRate.ClampSelection(selection); // CLIENT SEND RATE var ratioOptions = _reusableRatioGUIArrays[tickRate.Count - 1]; // _ratioOptions; var ratioValues = _reusableRatioIntArrays[tickRate.Count - 1]; //_ratioValues; for (var i = 0; i < tickRate.Count; ++i) { ratioOptions[i] = new GUIContent(tickRate.GetTickRate(i).ToString()); ratioValues[i] = i; } selection.ClientSendIndex = DrawPopup(ref labelRect, ref fieldRect, rowHeight, new GUIContent("Client Send Rate"), ratioValues, ratioValues, ratioOptions, selection.ClientSendIndex); // SERVER SIM RATE selection.ServerIndex = DrawPopup(ref labelRect, ref fieldRect, rowHeight, new GUIContent("Server Tick Rate"), ratioValues, ratioValues, ratioOptions, selection.ServerIndex); selection = tickRate.ClampSelection(selection); // SERVER SEND RATE - uses a subset of ratio - since it CANNOT be higher than Server Rate. var sOffset = selection.ServerIndex; var sLen = ratioOptions.Length - sOffset; var sSendOptions = _reusableServerGUIArrays[sLen - 1]; // new GUIContent[sLen]; var sSendValues = _reusableServerIntArrays[sLen - 1]; // new int[sLen]; for (var i = 0; i < sLen; ++i) { sSendOptions[i] = ratioOptions[i + sOffset]; sSendValues[i] = ratioValues[i + sOffset]; } selection.ServerSendIndex = DrawPopup(ref labelRect, ref fieldRect, rowHeight, new GUIContent("Server Send Rate"), ratioValues, sSendValues, sSendOptions, selection.ServerSendIndex, sOffset); if (hold.Equals(selection) == false) { selection = tickRate.ClampSelection(selection); // FIELD INFO SET VALUE ALTERNATIVE // fieldInfo.SetValue(targetObject, selection); clientRateProperty.intValue = selection.Client; clientSendProperty.intValue = selection.ClientSendIndex; serverRateProperty.intValue = selection.ServerIndex; serverSendProperty.intValue = selection.ServerSendIndex; property.serializedObject.ApplyModifiedProperties(); } } } private int GetIndexForClientRate(int clientRate) { for (var i = ClientRateValues.Length - 1; i >= 0; --i) if (_clientRateValues[i] == clientRate) { return i; } return -1; } // Extract in reverse order so all the popups are consistent. private static void ExtractClientRates() { int cnt = TickRate.Available.Count; _clientRateOptions = new GUIContent[cnt]; _clientRateValues = new int[cnt]; for (int i = 0, reverse = cnt -1; i < cnt; ++i, --reverse) { _clientRateOptions[i] = new GUIContent(TickRate.Available[reverse].Client.ToString()); _clientRateValues[i] = TickRate.Available[reverse].Client; } } // Wacky reflection to locate the value private static TickRate.Selection GetSelectionValue (SerializedProperty property) { object obj = property.serializedObject.targetObject; string path = property.propertyPath; string[] parts = path.Split ('.'); foreach (var t in parts) { obj = GetValueFromFieldName (t, obj); } return (TickRate.Selection)obj; } private static object GetValueFromFieldName(string name, object obj, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) { FieldInfo field = obj.GetType().GetField(name, bindings); if (field != null) { return field.GetValue(obj); } return TickRate.Default; } } } #endregion #region Assets/Photon/Fusion/Editor/DebugDllToggle.cs namespace Fusion.Editor { using System; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; public static class DebugDllToggle { const string FusionRuntimeDllGuid = "e725a070cec140c4caffb81624c8c787"; public static string[] FileList = new[] { "Fusion.Common.dll", "Fusion.Common.pdb", "Fusion.Runtime.dll", "Fusion.Runtime.pdb", "Fusion.Realtime.dll", "Fusion.Realtime.pdb", "Fusion.Sockets.dll", "Fusion.Sockets.pdb"}; [MenuItem("Tools/Fusion/Toggle Debug Dlls")] public static void Toggle() { // find the root string dir; { var fusionRuntimeDllPath = AssetDatabase.GUIDToAssetPath(FusionRuntimeDllGuid); if (string.IsNullOrEmpty(fusionRuntimeDllPath)) { Debug.LogError($"Cannot locate assemblies directory"); return; } else { dir = PathUtils.Normalize(Path.GetDirectoryName(fusionRuntimeDllPath)); } } var dllsAvailable = FileList.All(f => File.Exists($"{dir}/{f}")); var debugFilesAvailable = FileList.All(f => File.Exists($"{dir}/{f}.debug")); if (dllsAvailable == false) { Debug.LogError("Cannot find all fusion dlls"); return; } if (debugFilesAvailable == false) { Debug.LogError("Cannot find all specially marked .debug dlls"); return; } if (FileList.Any(f => new FileInfo($"{dir}/{f}.debug").Length == 0)) { Debug.LogError("Debug dlls are not valid"); return; } try { foreach (var f in FileList) { var tempFile = FileUtil.GetUniqueTempPathInProject(); FileUtil.MoveFileOrDirectory($"{dir}/{f}", tempFile); FileUtil.MoveFileOrDirectory($"{dir}/{f}.debug", $"{dir}/{f}"); FileUtil.MoveFileOrDirectory(tempFile, $"{dir}/{f}.debug"); File.Delete(tempFile); } if (new FileInfo($"{dir}/{FileList[0]}").Length > new FileInfo($"{dir}/{FileList[0]}.debug").Length) { Debug.Log("Activated Fusion DEBUG dlls"); } else { Debug.Log("Activated Fusion RELEASE dlls"); } } catch (Exception e) { Debug.LogAssertion(e); Debug.LogError($"Failed to rename files"); } AssetDatabase.Refresh(); } } } #endregion #region Assets/Photon/Fusion/Editor/EditorRecompileHook.cs namespace Fusion.Editor { using System; using System.IO; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; [InitializeOnLoad] public static class EditorRecompileHook { static EditorRecompileHook() { EditorApplication.update += delegate { if (PlayerSettings.allowUnsafeCode == false) { PlayerSettings.allowUnsafeCode = true; // request re-compile CompilationPipeline.RequestScriptCompilation(RequestScriptCompilationOptions.None); } }; AssemblyReloadEvents.beforeAssemblyReload += ShutdownRunners; CompilationPipeline.compilationStarted += _ => ShutdownRunners(); CompilationPipeline.compilationStarted += _ => StoreConfigPath(); } static void ShutdownRunners() { var runners = NetworkRunner.GetInstancesEnumerator(); while (runners.MoveNext()) { if (runners.Current) { runners.Current.Shutdown(); } } } static void StoreConfigPath() { const string ConfigPathCachePath = "Temp/FusionILWeaverConfigPath.txt"; var configPath = NetworkProjectConfigUtilities.GetGlobalConfigPath(); if (string.IsNullOrEmpty(configPath)) { // delete try { File.Delete(ConfigPathCachePath); } catch (FileNotFoundException) { // ok } catch (Exception ex) { FusionEditorLog.ErrorConfig($"Error when clearing the config path file for the Weaver. Weaving results may be invalid: {ex}"); } } else { try { System.IO.File.WriteAllText(ConfigPathCachePath, configPath); } catch (Exception ex) { FusionEditorLog.ErrorConfig($"Error when writing the config path file for the Weaver. Weaving results may be invalid: {ex}"); } } } } } #endregion #region Assets/Photon/Fusion/Editor/FusionAssistants.cs namespace Fusion.Editor { using UnityEngine; using System; static class FusionAssistants { public const int PRIORITY = 0; public const int PRIORITY_LOW = 1000; /// /// Ensure GameObject has component T. Will create as needed and return the found/created component. /// public static T EnsureComponentExists(this GameObject go) where T : Component { if (go.TryGetComponent(out var t)) return t; else return go.AddComponent(); } public static GameObject EnsureComponentsExistInScene(string preferredGameObjectName, params Type[] components) { GameObject go = null; foreach(var c in components) { var found = UnityEngine.Object.FindFirstObjectByType(c); if (found) continue; if (go == null) go = new GameObject(preferredGameObjectName); go.AddComponent(c); } return go; } public static T EnsureExistsInScene(string preferredGameObjectName = null, GameObject onThisObject = null, params Type[] otherRequiredComponents) where T : Component { if (preferredGameObjectName == null) preferredGameObjectName = typeof(T).Name; T comp; comp = UnityEngine.Object.FindFirstObjectByType(); if (comp == null) { // T was not found in scene, create a new gameobject and add T, as well as other required components if (onThisObject == null) onThisObject = new GameObject(preferredGameObjectName); comp = onThisObject.AddComponent(); foreach (var add in otherRequiredComponents) { onThisObject.AddComponent(add); } } else { // Make sure existing found T has the indicated extra components as well. foreach (var add in otherRequiredComponents) { if (comp.GetComponent(add) == false) comp.gameObject.AddComponent(add); } } return comp; } /// /// Create a scene object with all of the supplied arguments and parameters applied. /// public static GameObject CreatePrimitive( PrimitiveType? primitive, string name, Vector3? position, Quaternion? rotation, Vector3? scale, Transform parent, Material material, params Type[] addComponents) { GameObject go; if (primitive.HasValue) { go = GameObject.CreatePrimitive(primitive.Value); go.name = name; if (material != null) go.GetComponent().material = material; foreach (var type in addComponents) { go.AddComponent(type); } } else { go = new GameObject(name, addComponents); } if (position.HasValue) go.transform.position = position.Value; if (rotation.HasValue) go.transform.rotation = rotation.Value; if (scale.HasValue) go.transform.localScale = scale.Value; if (parent) go.transform.parent = parent; return go; } internal static EnableOnSingleRunner EnsureComponentHasVisibilityNode(this Component component) { var allExistingNodes = component.GetComponents(); foreach (var existingNodes in allExistingNodes) { foreach (var comp in existingNodes.Components) { if (comp == component) { return existingNodes; } } } // Component is not represented yet. If there is a VisNodes already, use it. Otherwise make one. EnableOnSingleRunner targetNodes = component.GetComponent(); if (targetNodes == null) { targetNodes = component.gameObject.AddComponent(); } // Add this component to the collection. int newArrayPos = targetNodes.Components.Length; Array.Resize(ref targetNodes.Components, newArrayPos + 1); targetNodes.Components[newArrayPos] = component; return targetNodes; } } } #endregion #region Assets/Photon/Fusion/Editor/FusionBootstrapEditor.cs namespace Fusion.Editor { using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; [CustomEditor(typeof(FusionBootstrap))] public class FusionBootstrapEditor : BehaviourEditor { public override void OnInspectorGUI() { base.OnInspectorGUI(); if (Application.isPlaying) return; var currentScene = SceneManager.GetActiveScene(); if (!currentScene.IsAddedToBuildSettings()) { using (new FusionEditorGUI.WarningScope("Current scene is not added to Build Settings list.")) { if (GUILayout.Button("Add Scene To Build Settings")) { if (currentScene.name == "") { UnityEditor.SceneManagement.EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); } if (currentScene.name != "") { EditorBuildSettings.scenes = EditorBuildSettings.scenes .Concat(new[] { new EditorBuildSettingsScene(currentScene.path, true) }) .ToArray(); } } } } } } } #endregion #region Assets/Photon/Fusion/Editor/FusionBuildTriggers.cs namespace Fusion.Editor { using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; public class FusionBuildTriggers : IPreprocessBuildWithReport { public const int CallbackOrder = 1000; public int callbackOrder => CallbackOrder; public void OnPreprocessBuild(BuildReport report) { if (report.summary.platformGroup != BuildTargetGroup.Standalone) { return; } if (!PlayerSettings.runInBackground) { FusionEditorLog.Warn($"Standalone builds should have {nameof(PlayerSettings)}.{nameof(PlayerSettings.runInBackground)} enabled. " + $"Otherwise, loss of application focus may result in connection termination."); } } } } #endregion #region Assets/Photon/Fusion/Editor/FusionEditor.Common.cs // merged Editor #region AssetDatabaseUtils.Addressables.cs #if (FUSION_ADDRESSABLES || FUSION_ENABLE_ADDRESSABLES) && !FUSION_DISABLE_ADDRESSABLES namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEngine; public partial class AssetDatabaseUtils { public static void AddAddressableAssetsWithLabelMonitor(string label, Action handler) { AddressableAssetSettings.OnModificationGlobal += (settings, modificationEvent, data) => { switch (modificationEvent) { case AddressableAssetSettings.ModificationEvent.EntryAdded: case AddressableAssetSettings.ModificationEvent.EntryCreated: case AddressableAssetSettings.ModificationEvent.EntryModified: case AddressableAssetSettings.ModificationEvent.EntryMoved: IEnumerable entries; if (data is AddressableAssetEntry singleEntry) { entries = Enumerable.Repeat(singleEntry, 1); } else { entries = (IEnumerable)data; } List allEntries = new List(); foreach (var entry in entries) { entry.GatherAllAssets(allEntries, true, true, true); if (allEntries.Any(x => HasLabel(x.AssetPath, label))) { handler(settings.currentHash); break; } allEntries.Clear(); } break; case AddressableAssetSettings.ModificationEvent.EntryRemoved: // TODO: check what has been removed handler(settings.currentHash); break; } }; } public static AddressableAssetEntry GetAddressableAssetEntry(UnityEngine.Object source) { if (source == null || !AssetDatabase.Contains(source)) { return null; } return GetAddressableAssetEntry(GetAssetGuidOrThrow(source)); } public static AddressableAssetEntry GetAddressableAssetEntry(string guid) { if (string.IsNullOrEmpty(guid)) { return null; } var addressableSettings = AddressableAssetSettingsDefaultObject.Settings; return addressableSettings.FindAssetEntry(guid); } public static AddressableAssetEntry CreateOrMoveAddressableAssetEntry(UnityEngine.Object source, string groupName = null) { if (source == null || !AssetDatabase.Contains(source)) return null; return CreateOrMoveAddressableAssetEntry(GetAssetGuidOrThrow(source), groupName); } public static AddressableAssetEntry CreateOrMoveAddressableAssetEntry(string guid, string groupName = null) { if (string.IsNullOrEmpty(guid)) { return null; } var addressableSettings = AddressableAssetSettingsDefaultObject.Settings; AddressableAssetGroup group; if (string.IsNullOrEmpty(groupName)) { group = addressableSettings.DefaultGroup; } else { group = addressableSettings.FindGroup(groupName); } if (group == null) { throw new ArgumentOutOfRangeException($"Group {groupName} not found"); } var entry = addressableSettings.CreateOrMoveEntry(guid, group); return entry; } public static bool RemoveMoveAddressableAssetEntry(UnityEngine.Object source) { if (source == null || !AssetDatabase.Contains(source)) { return false; } return RemoveMoveAddressableAssetEntry(GetAssetGuidOrThrow(source)); } public static bool RemoveMoveAddressableAssetEntry(string guid) { if (string.IsNullOrEmpty(guid)) { return false; } var addressableSettings = AddressableAssetSettingsDefaultObject.Settings; return addressableSettings.RemoveAssetEntry(guid); } } } #endif #endregion #region AssetDatabaseUtils.cs namespace Fusion.Editor { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Build; using UnityEditor.PackageManager; using UnityEngine; using Object = UnityEngine.Object; public static partial class AssetDatabaseUtils { public static void SetAssetAndTheMainAssetDirty(UnityEngine.Object obj) { EditorUtility.SetDirty(obj); var assetPath = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(assetPath)) { return; } var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath); if (!mainAsset || mainAsset == obj) { return; } EditorUtility.SetDirty(mainAsset); } public static string GetAssetPathOrThrow(int instanceID) { var result = AssetDatabase.GetAssetPath(instanceID); if (string.IsNullOrEmpty(result)) { throw new ArgumentException($"Asset with InstanceID {instanceID} not found"); } return result; } public static string GetAssetPathOrThrow(UnityEngine.Object obj) { var result = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(result)) { throw new ArgumentException($"Asset {obj} not found"); } return result; } public static string GetAssetPathOrThrow(string assetGuid) { var result = AssetDatabase.GUIDToAssetPath(assetGuid); if (string.IsNullOrEmpty(result)) { throw new ArgumentException($"Asset with Guid {assetGuid} not found"); } return result; } public static string GetAssetGuidOrThrow(string assetPath) { var result = AssetDatabase.AssetPathToGUID(assetPath); if (string.IsNullOrEmpty(result)) { throw new ArgumentException($"Asset with path {assetPath} not found"); } return result; } public static string GetAssetGuidOrThrow(int instanceId) { var assetPath = GetAssetPathOrThrow(instanceId); return GetAssetGuidOrThrow(assetPath); } public static string GetAssetGuidOrThrow(UnityEngine.Object obj) { var assetPath = GetAssetPathOrThrow(obj); return GetAssetGuidOrThrow(assetPath); } public static (string, long) GetGUIDAndLocalFileIdentifierOrThrow(LazyLoadReference reference) where T : UnityEngine.Object { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(reference, out var guid, out long localId)) { throw new ArgumentException($"Asset with instanceId {reference} not found"); } return (guid, localId); } public static (string, long) GetGUIDAndLocalFileIdentifierOrThrow(UnityEngine.Object obj) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long localId)) { throw new ArgumentException(nameof(obj)); } return (guid, localId); } public static (string, long) GetGUIDAndLocalFileIdentifierOrThrow(int instanceId) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(instanceId, out var guid, out long localId)) { throw new ArgumentException($"Asset with instanceId {instanceId} not found"); } return (guid, localId); } public static void MoveAssetOrThrow(string source, string destination) { var error = AssetDatabase.MoveAsset(source, destination); if (!string.IsNullOrEmpty(error)) { throw new ArgumentException($"Failed to move {source} to {destination}: {error}"); } } public static bool HasLabel(string assetPath, string label) { var guidStr = AssetDatabase.AssetPathToGUID(assetPath); if (!GUID.TryParse(guidStr, out var guid)) { return false; } var labels = AssetDatabase.GetLabels(guid); var index = Array.IndexOf(labels, label); return index >= 0; } public static bool HasLabel(UnityEngine.Object obj, string label) { var labels = AssetDatabase.GetLabels(obj); var index = Array.IndexOf(labels, label); return index >= 0; } public static bool HasLabel(GUID guid, string label) { var labels = AssetDatabase.GetLabels(guid); var index = Array.IndexOf(labels, label); return index >= 0; } public static bool HasAnyLabel(string assetPath, params string[] labels) { var guidStr = AssetDatabase.AssetPathToGUID(assetPath); if (!GUID.TryParse(guidStr, out var guid)) { return false; } var assetLabels = AssetDatabase.GetLabels(guid); foreach (var label in labels) { if (Array.IndexOf(assetLabels, label) >= 0) { return true; } } return false; } public static bool SetLabel(string assetPath, string label, bool present) { var guid = AssetDatabase.GUIDFromAssetPath(assetPath); if (guid.Empty()) { return false; } var labels = AssetDatabase.GetLabels(guid); var index = Array.IndexOf(labels, label); if (present) { if (index >= 0) { return false; } ArrayUtility.Add(ref labels, label); } else { if (index < 0) { return false; } ArrayUtility.RemoveAt(ref labels, index); } var obj = AssetDatabase.LoadMainAssetAtPath(assetPath); if (obj == null) { return false; } AssetDatabase.SetLabels(obj, labels); return true; } public static bool SetLabel(UnityEngine.Object obj, string label, bool present) { var labels = AssetDatabase.GetLabels(obj); var index = Array.IndexOf(labels, label); if (present) { if (index >= 0) { return false; } ArrayUtility.Add(ref labels, label); } else { if (index < 0) { return false; } ArrayUtility.RemoveAt(ref labels, index); } AssetDatabase.SetLabels(obj, labels); return true; } public static bool SetLabels(string assetPath, string[] labels) { var obj = AssetDatabase.LoadMainAssetAtPath(assetPath); if (obj == null) { return false; } AssetDatabase.SetLabels(obj, labels); return true; } public static bool HasScriptingDefineSymbol(BuildTargetGroup group, string value) { var defines = PlayerSettings.GetScriptingDefineSymbols(NamedBuildTarget.FromBuildTargetGroup(group)).Split(';'); return System.Array.IndexOf(defines, value) >= 0; } public static T SetScriptableObjectType(ScriptableObject obj) where T : ScriptableObject { return (T)SetScriptableObjectType(obj, typeof(T)); } public static ScriptableObject SetScriptableObjectType(ScriptableObject obj, Type type) { const string ScriptPropertyName = "m_Script"; if (!obj) { throw new ArgumentNullException(nameof(obj)); } if (type == null) { throw new ArgumentNullException(nameof(type)); } if (!type.IsSubclassOf(typeof(ScriptableObject))) { throw new ArgumentException($"Type {type} is not a subclass of {nameof(ScriptableObject)}"); } if (obj.GetType() == type) { return obj; } var tmp = ScriptableObject.CreateInstance(type); try { using (var dst = new SerializedObject(obj)) { using (var src = new SerializedObject(tmp)) { var scriptDst = dst.FindPropertyOrThrow(ScriptPropertyName); var scriptSrc = src.FindPropertyOrThrow(ScriptPropertyName); Debug.Assert(scriptDst.objectReferenceValue != scriptSrc.objectReferenceValue); dst.CopyFromSerializedProperty(scriptSrc); dst.ApplyModifiedPropertiesWithoutUndo(); return (ScriptableObject)dst.targetObject; } } } finally { UnityEngine.Object.DestroyImmediate(tmp); } } private static bool IsEnumValueObsolete(string valueName) where T : System.Enum { var fi = typeof(T).GetField(valueName); var attributes = fi.GetCustomAttributes(typeof(System.ObsoleteAttribute), false); return attributes?.Length > 0; } private static IEnumerable ValidBuildTargetGroups { get { foreach (var name in System.Enum.GetNames(typeof(BuildTargetGroup))) { if (IsEnumValueObsolete(name)) continue; var group = (BuildTargetGroup)System.Enum.Parse(typeof(BuildTargetGroup), name); if (group == BuildTargetGroup.Unknown) continue; yield return group; } } } public static bool? HasScriptingDefineSymbol(string value) { bool anyDefined = false; bool anyUndefined = false; foreach (BuildTargetGroup group in ValidBuildTargetGroups) { if (HasScriptingDefineSymbol(group, value)) { anyDefined = true; } else { anyUndefined = true; } } return (anyDefined && anyUndefined) ? (bool?)null : anyDefined; } public static void UpdateScriptingDefineSymbol(BuildTargetGroup group, string define, bool enable) { UpdateScriptingDefineSymbolInternal(new[] { group }, enable ? new[] { define } : null, enable ? null : new[] { define }); } public static void UpdateScriptingDefineSymbol(string define, bool enable) { UpdateScriptingDefineSymbolInternal(ValidBuildTargetGroups, enable ? new[] { define } : null, enable ? null : new[] { define }); } public static void UpdateScriptingDefineSymbol(BuildTargetGroup group, IEnumerable definesToAdd, IEnumerable definesToRemove) { UpdateScriptingDefineSymbolInternal(new[] { group }, definesToAdd, definesToRemove); } public static void UpdateScriptingDefineSymbol(IEnumerable definesToAdd, IEnumerable definesToRemove) { UpdateScriptingDefineSymbolInternal(ValidBuildTargetGroups, definesToAdd, definesToRemove); } private static void UpdateScriptingDefineSymbolInternal(IEnumerable groups, IEnumerable definesToAdd, IEnumerable definesToRemove) { EditorApplication.LockReloadAssemblies(); try { foreach (var group in groups) { var originalDefines = PlayerSettings.GetScriptingDefineSymbols(NamedBuildTarget.FromBuildTargetGroup(group)); var defines = originalDefines.Split(';').ToList(); if (definesToRemove != null) { foreach (var d in definesToRemove) { defines.Remove(d); } } if (definesToAdd != null) { foreach (var d in definesToAdd) { defines.Remove(d); defines.Add(d); } } var newDefines = string.Join(";", defines); PlayerSettings.SetScriptingDefineSymbols(NamedBuildTarget.FromBuildTargetGroup(group), newDefines); } } finally { EditorApplication.UnlockReloadAssemblies(); } } public static AssetEnumerable IterateAssets(string root = null, string label = null) where T : UnityEngine.Object { return IterateAssets(root, label, typeof(T)); } public static AssetEnumerable IterateAssets(string root = null, string label = null, Type type = null) { return new AssetEnumerable(root, label, type); } public struct AssetEnumerator : IEnumerator { private HierarchyProperty _hierarchyProperty; private int _rootFolderIndex; private readonly string[] _rootFolders; private readonly string _searchFilter; private static bool IsPackageHidden(UnityEditor.PackageManager.PackageInfo info) => info.type == "module" || info.type == "feature" && info.source != PackageSource.Embedded; public AssetEnumerator(string root, string label, Type type) { { string searchFilter; if (type == typeof(GameObject)) { searchFilter = "t:prefab"; } else if (type != null) { searchFilter = "t:" + type.FullName; } else { searchFilter = ""; } if (!string.IsNullOrEmpty(label)) { if (searchFilter.Length > 0) { searchFilter += " "; } searchFilter += "l:" + label; } _searchFilter = searchFilter; } _rootFolderIndex = 0; if (string.IsNullOrEmpty(root)) { // search everywhere _rootFolders = new[] { "Assets" }.Concat(UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages() .Where(x => !IsPackageHidden(x)) .Select(x => x.assetPath)) .ToArray(); _hierarchyProperty = new HierarchyProperty(_rootFolders[0]); } else { _rootFolders = null; _hierarchyProperty = new HierarchyProperty(root); } _hierarchyProperty.SetSearchFilter(_searchFilter, (int)SearchableEditorWindow.SearchMode.All); } public bool MoveNext() { if (_hierarchyProperty.Next(null)) { return true; } if (_rootFolders == null || _rootFolderIndex + 1 >= _rootFolders.Length) { return false; } var newHierarchyProperty = new HierarchyProperty(_rootFolders[++_rootFolderIndex]); newHierarchyProperty.SetSearchFilter(_searchFilter, (int)SearchableEditorWindow.SearchMode.All); _hierarchyProperty = newHierarchyProperty; // try again return MoveNext(); } public void Reset() { throw new System.NotImplementedException(); } public HierarchyProperty Current => _hierarchyProperty; object IEnumerator.Current => Current; public void Dispose() { } } public struct AssetEnumerable : IEnumerable { private readonly string _root; private readonly string _label; private readonly Type _type; public AssetEnumerable(string root, string label, Type type) { _type = type; _root = root; _label = label; } public AssetEnumerator GetEnumerator() => new AssetEnumerator(_root, _label, _type); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } } #endregion #region EditorButtonDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; public struct EditorButtonDrawer { private struct ButtonEntry { public MethodInfo Method; public GUIContent Content; public EditorButtonAttribute Attribute; public (DoIfAttributeBase, Func)[] DoIfs; } private Editor _lastEditor; private List _buttons; public void Draw(Editor editor) { var targets = editor.targets; if (_lastEditor != editor) { _lastEditor = editor; Refresh(editor); } if (_buttons == null || targets == null || targets.Length == 0) { return; } foreach (var entry in _buttons) { if (entry.Attribute.Visibility == EditorButtonVisibility.PlayMode && !EditorApplication.isPlaying) { continue; } if (entry.Attribute.Visibility == EditorButtonVisibility.EditMode && EditorApplication.isPlaying) { continue; } if (!entry.Attribute.AllowMultipleTargets && editor.targets.Length > 1) { continue; } bool readOnly = false; bool hidden = false; string warningMessage = null; bool warningAsBox = false; foreach (var (doIf, getter) in entry.DoIfs) { bool checkResult; if (getter == null) { checkResult = DoIfAttributeDrawer.CheckDraw(doIf, editor.serializedObject); } else { var value = getter(targets[0]); checkResult = DoIfAttributeDrawer.CheckCondition(doIf, value); } if (!checkResult) { if (doIf is DrawIfAttribute drawIf) { if (drawIf.Hide) { hidden = true; break; } else { readOnly = true; } } else if (doIf is WarnIfAttribute warnIf) { warningMessage = warnIf.Message; warningAsBox = warnIf.AsBox; } } } if (hidden) { continue; } using (warningMessage == null ? null : (IDisposable)new FusionEditorGUI.WarningScope(warningMessage)) { var rect = FusionEditorGUI.LayoutHelpPrefix(editor, entry.Method); using (new EditorGUI.DisabledScope(readOnly)) { if (GUI.Button(rect, entry.Content)) { EditorGUI.BeginChangeCheck(); if (entry.Method.IsStatic) { entry.Method.Invoke(null, null); } else { foreach (var target in targets) { entry.Method.Invoke(target, null); if (entry.Attribute.DirtyObject) { EditorUtility.SetDirty(target); } } } if (EditorGUI.EndChangeCheck()) { editor.serializedObject.Update(); } } } } } } private void Refresh(Editor editor) { if (editor == null) { throw new ArgumentNullException(nameof(editor)); } var targetType = editor.target.GetType(); _buttons = targetType .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy) .Where(x => x.GetParameters().Length == 0 && x.IsDefined(typeof(EditorButtonAttribute))) .Select(method => { var attribute = method.GetCustomAttribute(); var label = new GUIContent(attribute.Label ?? ObjectNames.NicifyVariableName(method.Name)); var drawIfs = method.GetCustomAttributes() .Select(x => { var prop = editor.serializedObject.FindProperty(x.ConditionMember); return prop != null ? (x, null) : (x, targetType.CreateGetter(x.ConditionMember)); }) .ToArray(); return new ButtonEntry() { Attribute = attribute, Content = label, Method = method, DoIfs = drawIfs, }; }) .OrderBy(x => x.Attribute.Priority) .ToList(); } } } #endregion #region EnumDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; public struct EnumDrawer { private Mask256[] _values; private string[] _names; private bool _isFlags; private Type _enumType; private Mask256 _allBitMask; private FieldInfo[] _fields; [NonSerialized] private List _selectedIndices; public Mask256[] Values => _values; public string[] Names => _names; public bool IsFlags => _isFlags; public Type EnumType => _enumType; public Mask256 BitMask => _allBitMask; public FieldInfo[] Fields => _fields; public bool EnsureInitialized(Type enumType, bool includeFields) { if (enumType == null) { throw new ArgumentNullException(nameof(enumType)); } bool isEnum = enumType.IsEnum; if (!isEnum && !typeof(FieldsMask).IsAssignableFrom(enumType)) { throw new ArgumentException("Type must be an enum or FieldsMask", nameof(enumType)); } // Already initialized if (_enumType == enumType) { return false; } if (isEnum) { var enumUnderlyingType = Enum.GetUnderlyingType(enumType); var rawValues = Enum.GetValues(enumType); _fields = includeFields ? new FieldInfo[rawValues.Length] : null; _names = Enum.GetNames(enumType); _values = new Mask256[rawValues.Length]; _isFlags = enumType.GetCustomAttribute() != null; _enumType = enumType; for (int i = 0; i < rawValues.Length; ++i) { if (enumUnderlyingType == typeof(int) || enumUnderlyingType == typeof(long) || enumUnderlyingType == typeof(short) || enumUnderlyingType == typeof(byte)) { _values[i] = Convert.ToInt64(rawValues.GetValue(i)); } else { _values[i] = unchecked((long)Convert.ToUInt64(rawValues.GetValue(i))); } _allBitMask[0] |= _values[i][0]; if (includeFields) { _fields[i] = enumType.GetField(_names[i], BindingFlags.Static | BindingFlags.Public); } } } else { // Handling for FieldsMask var tType = enumType.GenericTypeArguments[0]; _fields = tType.GetFields(); _names = new string[_fields.Length]; _values = new Mask256[_fields.Length]; _isFlags = true; _enumType = enumType; for (int i = 0; i < _values.Length; i++) { long value = (long)1 << i; _allBitMask.SetBit(i, true);; _values[i].SetBit(i, true); // = (long)1 << i; _names[i] = _fields[i].Name; } } for (int i = 0; i < _names.Length; ++i) { _names[i] = ObjectNames.NicifyVariableName(_names[i]); } return true; } public void Draw(Rect position, SerializedProperty property, Type enumType, bool isEnum) { if (property == null) { throw new ArgumentNullException(nameof(property)); } EnsureInitialized(enumType, false); Mask256 currentValue; if (isEnum) { currentValue = new Mask256( property.longValue ); } else { currentValue = new Mask256( property.GetFixedBufferElementAtIndex(0).longValue, property.GetFixedBufferElementAtIndex(1).longValue, property.GetFixedBufferElementAtIndex(2).longValue, property.GetFixedBufferElementAtIndex(3).longValue ); } _selectedIndices ??= new List(); _selectedIndices.Clear(); // find out what to show for (int i = 0; i < _values.Length; ++i) { var value = _values[i]; if (_isFlags == false) { if (currentValue[0]== value[0]) { _selectedIndices.Add(i); break; } } else if (Equals(currentValue & value, value)) { _selectedIndices.Add(i); } } string labelValue; if (_selectedIndices.Count == 0) { if (_isFlags && currentValue.IsNothing()) { labelValue = "Nothing"; } else { labelValue = ""; } } else if (_selectedIndices.Count == 1) { labelValue = _names[_selectedIndices[0]]; } else { Debug.Assert(_isFlags); if (_selectedIndices.Count == _values.Length) { labelValue = "Everything"; } else { var names = _names; labelValue = string.Join(", ", _selectedIndices.Select(x => names[x])); } } if (EditorGUI.DropdownButton(position, new GUIContent(labelValue), FocusType.Keyboard)) { var values = _values; var indices = _selectedIndices; if (_isFlags) { var allOptions = new[] { "Nothing", "Everything" }.Concat(_names).ToArray(); List allIndices = new List(); if (_selectedIndices.Count == 0) { allIndices.Add(0); // nothing } else if (_selectedIndices.Count == _values.Length) { allIndices.Add(1); // everything } allIndices.AddRange(_selectedIndices.Select(x => x + 2)); UnityInternal.EditorUtility.DisplayCustomMenu(position, allOptions, allIndices.ToArray(), (userData, options, selected) => { if (selected == 0) { // Clicked None if (isEnum) { property.longValue = 0; } else { property.GetFixedBufferElementAtIndex(0).longValue = 0; property.GetFixedBufferElementAtIndex(1).longValue = 0; property.GetFixedBufferElementAtIndex(2).longValue = 0; property.GetFixedBufferElementAtIndex(3).longValue = 0; } } else if (selected == 1) { // Selected Everything if (isEnum) { property.longValue = 0; } else { property.GetFixedBufferElementAtIndex(0).longValue = 0; property.GetFixedBufferElementAtIndex(1).longValue = 0; property.GetFixedBufferElementAtIndex(2).longValue = 0; property.GetFixedBufferElementAtIndex(3).longValue = 0; } foreach (var value in values) { if (isEnum) { property.longValue |= value[0]; } else{ property.GetFixedBufferElementAtIndex(0).longValue |= value[0]; property.GetFixedBufferElementAtIndex(1).longValue |= value[1]; property.GetFixedBufferElementAtIndex(2).longValue |= value[2]; property.GetFixedBufferElementAtIndex(3).longValue |= value[3]; } } } else { // Toggled a value selected -= 2; if (indices.Contains(selected)) { if (isEnum) { property.longValue &= ~values[selected][0]; } else { property.GetFixedBufferElementAtIndex(0).longValue &= ~values[selected][0]; property.GetFixedBufferElementAtIndex(1).longValue &= ~values[selected][1]; property.GetFixedBufferElementAtIndex(2).longValue &= ~values[selected][2]; property.GetFixedBufferElementAtIndex(3).longValue &= ~values[selected][3]; } } else { if (isEnum) { property.longValue |= (long)values[selected][0]; } else { property.GetFixedBufferElementAtIndex(0).longValue |= (long)values[selected][0]; property.GetFixedBufferElementAtIndex(1).longValue |= (long)values[selected][1]; property.GetFixedBufferElementAtIndex(2).longValue |= (long)values[selected][2]; property.GetFixedBufferElementAtIndex(3).longValue |= (long)values[selected][3]; } } } property.serializedObject.ApplyModifiedProperties(); }, null); } else { // non-flags enum UnityInternal.EditorUtility.DisplayCustomMenu(position, _names, _selectedIndices.ToArray(), (userData, options, selected) => { if (!indices.Contains(selected)) { property.longValue = values[selected][0]; property.serializedObject.ApplyModifiedProperties(); } }, null); } } } } } #endregion #region HashCodeUtilities.cs namespace Fusion.Editor { internal static class HashCodeUtilities { public const int InitialHash = (5381 << 16) + 5381; /// /// This may only be deterministic on 64 bit systems. /// /// /// /// public static int GetHashDeterministic(this string str, int initialHash = InitialHash) { unchecked { var hash1 = initialHash; var hash2 = initialHash; for (var i = 0; i < str.Length; i += 2) { hash1 = ((hash1 << 5) + hash1) ^ str[i]; if (i == str.Length - 1) { break; } hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; } return hash1 + hash2 * 1566083941; } } public static int CombineHashCodes(int a, int b) { return ((a << 5) + a) ^ b; } public static int CombineHashCodes(int a, int b, int c) { var t = ((a << 5) + a) ^ b; return ((t << 5) + t) ^ c; } public static unsafe int GetArrayHashCode(T* ptr, int length, int initialHash = InitialHash) where T : unmanaged { var hash = initialHash; for (var i = 0; i < length; ++i) { hash = hash * 31 + ptr[i].GetHashCode(); } return hash; } public static int GetHashCodeDeterministic(byte[] data, int initialHash = 0) { var hash = initialHash; for (var i = 0; i < data.Length; ++i) { hash = hash * 31 + data[i]; } return hash; } public static int GetHashCodeDeterministic(string data, int initialHash = 0) { var hash = initialHash; for (var i = 0; i < data.Length; ++i) { hash = hash * 31 + data[i]; } return hash; } public static unsafe int GetHashCodeDeterministic(T data, int initialHash = 0) where T : unmanaged { return GetHashCodeDeterministic(&data, initialHash); } public static unsafe int GetHashCodeDeterministic(T* data, int initialHash = 0) where T : unmanaged { var hash = initialHash; var ptr = (byte*)data; for (var i = 0; i < sizeof(T); ++i) { hash = hash * 31 + ptr[i]; } return hash; } } } #endregion #region LazyAsset.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using UnityEngine; using Object = UnityEngine.Object; public class LazyAsset { private T _value; private Func _factory; public LazyAsset(Func factory) { _factory = factory; } public T Value { get { if (NeedsUpdate) { lock (_factory) { if (NeedsUpdate) { _value = _factory(); } } } return _value; } } public static implicit operator T(LazyAsset lazyAsset) { return lazyAsset.Value; } public bool NeedsUpdate { get { if (_value is UnityEngine.Object obj) { return !obj; } else { return _value == null; } } } } public class LazyGUIStyle { private Func, GUIStyle> _factory; private GUIStyle _value; private List _dependencies = new List(); public LazyGUIStyle(Func, GUIStyle> factory) { _factory = factory; } public static LazyGUIStyle Create(Func, GUIStyle> factory) { return new LazyGUIStyle(factory); } public static implicit operator GUIStyle(LazyGUIStyle lazyAsset) { return lazyAsset.Value; } public GUIStyle Value { get { if (NeedsUpdate) { lock (_factory) { if (NeedsUpdate) { _dependencies.Clear(); _value = _factory(_dependencies); } } } return _value; } } public bool NeedsUpdate { get { if (_value == null) { return true; } foreach (var dependency in _dependencies) { if (!dependency) { return true; } } return false; } } public Vector2 CalcSize(GUIContent content) => Value.CalcSize(content); public void Draw(Rect position, GUIContent content, bool isHover, bool isActive, bool on, bool hasKeyboardFocus) => Value.Draw(position, content, isHover, isActive, on, hasKeyboardFocus); public void Draw(Rect position, bool isHover, bool isActive, bool on, bool hasKeyboardFocus) => Value.Draw(position, isHover, isActive, on, hasKeyboardFocus); } public class LazyGUIContent { private Func, GUIContent> _factory; private GUIContent _value; private List _dependencies = new List(); public LazyGUIContent(Func, GUIContent> factory) { _factory = factory; } public static LazyGUIContent Create(Func, GUIContent> factory) { return new LazyGUIContent(factory); } public static implicit operator GUIContent(LazyGUIContent lazyAsset) { return lazyAsset.Value; } public GUIContent Value { get { if (NeedsUpdate) { lock (_factory) { if (NeedsUpdate) { _dependencies.Clear(); _value = _factory(_dependencies); } } } return _value; } } public bool NeedsUpdate { get { if (_value == null) { return true; } foreach (var dependency in _dependencies) { if (!dependency) { return true; } } return false; } } } public static class LazyAsset { public static LazyAsset Create(Func factory) { return new LazyAsset(factory); } } } #endregion #region PathUtils.cs namespace Fusion.Editor { using System; // TODO: this should be moved to the runtime part static partial class PathUtils { public static bool TryMakeRelativeToFolder(string path, string folderWithSlashes, out string result) { var index = path.IndexOf(folderWithSlashes, StringComparison.Ordinal); if (index < 0) { result = string.Empty; return false; } if (folderWithSlashes[0] != '/' && index > 0) { result = string.Empty; return false; } result = path.Substring(index + folderWithSlashes.Length); return true; } [Obsolete("Use " + nameof(TryMakeRelativeToFolder) + " instead")] public static bool MakeRelativeToFolder(string path, string folder, out string result) { result = string.Empty; var formattedPath = Normalize(path); if (formattedPath.Equals(folder, StringComparison.Ordinal) || formattedPath.EndsWith("/" + folder)) { return true; } var index = formattedPath.IndexOf(folder + "/", StringComparison.Ordinal); var size = folder.Length + 1; if (index >= 0 && formattedPath.Length >= size) { result = formattedPath.Substring(index + size, formattedPath.Length - index - size); return true; } return false; } [Obsolete("Use Normalize instead")] public static string MakeSane(string path) { return Normalize(path); } public static string Normalize(string path) { return path.Replace("\\", "/").Replace("//", "/").TrimEnd('\\', '/').TrimStart('\\', '/'); } public static string GetPathWithoutExtension(string path) { if (path == null) return null; int length; if ((length = path.LastIndexOf('.')) == -1) return path; return path.Substring(0, length); } } } #endregion #region FusionCodeDoc.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Xml; using UnityEditor; using UnityEngine; public static class FusionCodeDoc { public const string Label = "FusionCodeDoc"; public const string Extension = "xml"; public const string ExtensionWithDot = "." + Extension; private static Dictionary _parsedCodeDocs = new(); private static Dictionary<(string assemblyName, string memberKey), (GUIContent withoutType, GUIContent withType)> _guiContentCache = new(); private static string CrefColor => EditorGUIUtility.isProSkin ? "#FFEECC" : "#664400"; public static GUIContent FindEntry(MemberInfo member, bool addTypeInfo = true) { switch (member) { case FieldInfo field: return FindEntry(field, addTypeInfo); case PropertyInfo property: return FindEntry(property); case MethodInfo method: return FindEntry(method); case Type type: return FindEntry(type); default: throw new ArgumentOutOfRangeException(nameof(member)); } } public static GUIContent FindEntry(FieldInfo field, bool addTypeInfo = true) { if (field == null) { throw new ArgumentNullException(nameof(field)); } return FindEntry(field, $"F:{SanitizeTypeName(field.DeclaringType)}.{field.Name}", addTypeInfo); } public static GUIContent FindEntry(PropertyInfo property) { if (property == null) { throw new ArgumentNullException(nameof(property)); } return FindEntry(property, $"P:{SanitizeTypeName(property.DeclaringType)}.{property.Name}"); } public static GUIContent FindEntry(MethodInfo method) { if (method == null) { throw new ArgumentNullException(nameof(method)); } return FindEntry(method, $"M:{SanitizeTypeName(method.DeclaringType)}.{method.Name}"); } public static GUIContent FindEntry(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } return FindEntry(type, $"T:{SanitizeTypeName(type)}"); } private static GUIContent FindEntry(MemberInfo member, string key, bool addTypeInfo = true) { Assembly assembly; if (member is Type type) { assembly = type.Assembly; } else { assembly = member.DeclaringType.Assembly; } var assemblyName = assembly.GetName().Name; if (_guiContentCache.TryGetValue((assemblyName, key), out var content)) { return addTypeInfo ? content.withType : content.withoutType; } foreach (var path in AssetDatabase.FindAssets($"l:{Label} t:TextAsset") .Select(x => AssetDatabase.GUIDToAssetPath(x)) .Where(x => Path.GetFileNameWithoutExtension(x).Contains(assemblyName, StringComparison.OrdinalIgnoreCase))) { // has this path been parsed already? if (!_parsedCodeDocs.TryGetValue(path, out var parsedCodeDoc)) { _parsedCodeDocs.Add(path, null); if (!string.IsNullOrEmpty(path)) { FusionEditorLog.Trace($"Trying to parse {path} for {key}"); if (TryParseCodeDoc(path, out parsedCodeDoc)) { _parsedCodeDocs[path] = parsedCodeDoc; } } else { FusionEditorLog.Trace($"Code doc for {assemblyName} not found."); } } if (parsedCodeDoc?.AssemblyName == assemblyName) { // code doc entries are sorted, so binary search is possible if (TryFindEntry(parsedCodeDoc, key, out var entry)) { content.withoutType = new GUIContent(entry.Summary, entry.Tooltip); content.withType = content.withoutType; _guiContentCache.Add((assemblyName, key), content); Type returnType = null; if (member is FieldInfo field) { returnType = field.FieldType; } else if (member is PropertyInfo property) { returnType = property.PropertyType; } if (returnType != null) { var typeEntry = FindEntry(returnType); string typeSummary = ""; if (typeEntry != null) { typeSummary += $"\n\n[{returnType.Name}] {typeEntry}"; } if (returnType.IsEnum) { // find all the enum values foreach (var enumValue in returnType.GetFields(BindingFlags.Static | BindingFlags.Public)) { var enumValueEntry = FindEntry(enumValue, addTypeInfo: false); if (enumValueEntry != null) { typeSummary += $"\n\n[{returnType.Name}.{enumValue.Name}] {enumValueEntry}"; } } } if (typeSummary.Length > 0) { content.withType = new GUIContent((entry.Summary + typeSummary).Trim('\n'), entry.Tooltip); _guiContentCache[(assemblyName, key)] = content; } } return addTypeInfo ? content.withType : content.withoutType;; } } } _guiContentCache.Add((assemblyName, key), default); return addTypeInfo ? content.withType : content.withoutType; bool TryFindEntry(CodeDoc codeDoc, string key, out Entry entry) { var searchEntry = EntryKeyComparer.DummyEntry; searchEntry.Key = key; var index = Array.BinarySearch(codeDoc.Entries, searchEntry, EntryKeyComparer.Instance); if (index < 0) { entry = null; return false; } entry = codeDoc.Entries[index]; return true; } } private static string SanitizeTypeName(Type type) { return type.FullName.Replace('+', '.'); } public static void InvalidateCache() { _parsedCodeDocs.Clear(); _guiContentCache.Clear(); } private static bool TryParseCodeDoc(string path, out CodeDoc result) { var xmlDoc = new XmlDocument(); try { xmlDoc.Load(path); } catch (Exception e) { FusionEditorLog.Error($"Failed to load {path}: {e}"); result = null; return false; } var assemblyName = xmlDoc.DocumentElement.SelectSingleNode("assembly") ?.SelectSingleNode("name") ?.FirstChild ?.Value; if (assemblyName == null) { result = null; return false; } var members = xmlDoc.DocumentElement.SelectSingleNode("members") .SelectNodes("member"); var buffer = new List(); foreach (XmlNode node in members) { var key = node.Attributes["name"].Value; var summary = node.SelectSingleNode("summary")?.InnerXml.Trim(); if (summary == null) { continue; } // remove generic indicator summary = summary.Replace("`1", ""); // fork tooltip and help summaries var help = Reformat(summary, false); var tooltip = Reformat(summary, true); buffer.Add(new FusionCodeDoc.Entry() { Key = key, Summary = help, Tooltip = tooltip }); } result = new CodeDoc() { AssemblyName = assemblyName, Entries = buffer.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase).ToArray() }; return true; } private static string Reformat(string summary, bool forTooltip) { // Tooltips don't support formatting tags. Inline help does. if (forTooltip) { summary = Regexes.SeeWithCref.Replace(summary, "$1"); summary = Regexes.See.Replace(summary, "$1"); summary = Regexes.XmlEmphasizeBrackets.Replace(summary, "$1"); } else { var colorstring = $"$1"; summary = Regexes.SeeWithCref.Replace(summary, colorstring); summary = Regexes.See.Replace(summary, colorstring); } summary = Regexes.XmlCodeBracket.Replace(summary, "$1"); // Reduce all sequential whitespace characters into a single space. summary = Regexes.WhitespaceString.Replace(summary, " "); // Turn and
into line breaks summary = Regex.Replace(summary, @"
\s?", "\n\n"); // prevent back to back paras from producing 4 line returns. summary = Regex.Replace(summary, @"\s?", "\n\n"); summary = Regex.Replace(summary, @"\s?", "\n\n"); // unescape <> summary = summary.Replace("<", "<"); summary = summary.Replace(">", ">"); summary = summary.Replace("&", "&"); summary = summary.Trim(); return summary; } private class Entry { public string Key; public string Summary; public string Tooltip; } private class CodeDoc { public string AssemblyName; public Entry[] Entries; } private class Postprocessor : AssetPostprocessor { private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (var path in importedAssets) { if (!path.StartsWith("Assets/") || !path.EndsWith(ExtensionWithDot)) { continue; } if (AssetDatabaseUtils.HasLabel(path, Label)) { FusionEditorLog.Trace($"Code doc {path} was imported, refreshing"); InvalidateCache(); } // is there a dll with the same name? if (File.Exists(path.Substring(0, path.Length - ExtensionWithDot.Length) + ".dll")) { FusionEditorLog.Trace($"Detected a dll next to {path}, applying label and refreshing."); AssetDatabaseUtils.SetLabel(path, Label, true); InvalidateCache(); } } } } private class EntryKeyComparer : IComparer { public static readonly EntryKeyComparer Instance = new EntryKeyComparer(); public static readonly Entry DummyEntry = new Entry(); public int Compare(Entry x, Entry y) { return string.Compare(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); } } private static class Regexes { public static readonly Regex SeeWithCref = new(@"", RegexOptions.None); public static readonly Regex See = new(@"([\w\.\d]*)<\/see\w*>", RegexOptions.None); public static readonly Regex WhitespaceString = new(@"\s+"); public static readonly Regex XmlCodeBracket = new(@"([\s\S]*?)"); public static readonly Regex XmlEmphasizeBrackets = new(@"<\w>([\s\S]*?)"); } } } #endregion #region FusionEditor.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; public abstract class FusionEditor : #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED Sirenix.OdinInspector.Editor.OdinEditor #else UnityEditor.Editor #endif { private EditorButtonDrawer _buttonDrawer; protected void PrepareOnInspectorGUI() { FusionEditorGUI.InjectScriptHeaderDrawer(this); } protected void DrawEditorButtons() { _buttonDrawer.Draw(this); } public override void OnInspectorGUI() { PrepareOnInspectorGUI(); base.OnInspectorGUI(); DrawEditorButtons(); } protected void DrawScriptPropertyField() { FusionEditorGUI.ScriptPropertyField(this); } #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED public new bool DrawDefaultInspector() { EditorGUI.BeginChangeCheck(); base.DrawDefaultInspector(); return EditorGUI.EndChangeCheck(); } #else protected virtual void OnEnable() { } protected virtual void OnDisable() { } #endif } } #endregion #region FusionEditorGUI.InlineHelp.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; public static partial class FusionEditorGUI { private const float SCROLL_WIDTH = 16f; private const float LEFT_HELP_INDENT = 8f; private static (object, string) s_expandedHelp; internal static Rect GetInlineHelpButtonRect(Rect position, bool expectFoldout = true, bool forScriptHeader = false) { var style = FusionEditorSkin.HelpButtonStyle; float width = style.fixedWidth <= 0 ? 16.0f : style.fixedWidth; float height = style.fixedHeight <= 0 ? 16.0f : style.fixedHeight; // this 2 lower than line height, but makes it look better const float FirstLineHeight = 16; int offsetY = forScriptHeader ? -1 : 1; var buttonRect = new Rect(position.x - width, position.y + (FirstLineHeight - height) / 2 + + offsetY, width, height); using (new EditorGUI.IndentLevelScope(expectFoldout ? -1 : 0)) { buttonRect.x = EditorGUI.IndentedRect(buttonRect).x; // give indented items a little extra padding - no need for them to be so crammed if (buttonRect.x > 8) { buttonRect.x -= 2; } } return buttonRect; } internal static bool DrawInlineHelpButton(Rect buttonRect, bool state, bool doButton = true, bool doIcon = true) { var style = FusionEditorSkin.HelpButtonStyle; var result = false; if (doButton) { EditorGUIUtility.AddCursorRect(buttonRect, MouseCursor.Link); using (new EnabledScope(true)) { result = GUI.Button(buttonRect, state ? InlineHelpStyle.HideInlineContent : InlineHelpStyle.ShowInlineContent, GUIStyle.none); } } if (doIcon) { // paint over what the inspector has drawn if (Event.current.type == EventType.Repaint) { style.Draw(buttonRect, false, false, state, false); } } return result; } internal static Vector2 GetInlineBoxSize(GUIContent content) { // const int InlineBoxExtraHeight = 4; var outerStyle = FusionEditorSkin.InlineBoxFullWidthStyle; var innerStyle = FusionEditorSkin.RichLabelStyle; var outerMargin = outerStyle.margin; var outerPadding = outerStyle.padding; var width = UnityInternal.EditorGUIUtility.contextWidth - outerMargin.left - outerMargin.right; // well... we do this, because there's no way of knowing the indent and scroll bar existence // when property height is calculated width -= 25.0f; if (content == null || width <= 0) { return default; } width -= outerPadding.left + outerPadding.right; var height = innerStyle.CalcHeight(content, width); // assume min height height = Mathf.Max(height, EditorGUIUtility.singleLineHeight); // add back all the padding height += outerPadding.top + outerPadding.bottom; height += outerMargin.top + outerMargin.bottom; return new Vector2(width, Mathf.Max(0, height)); } internal static Rect DrawInlineBoxUnderProperty(GUIContent content, Rect propertyRect, Color color, bool drawSelector = false, bool hasFoldout = false) { using (new EnabledScope(true)) { var boxSize = GetInlineBoxSize(content); if (Event.current.type == EventType.Repaint && boxSize.y > 0) { var boxMargin = FusionEditorSkin.InlineBoxFullWidthStyle.margin; var boxRect = new Rect() { x = boxMargin.left, y = propertyRect.yMax - boxSize.y, width = UnityInternal.EditorGUIUtility.contextWidth - boxMargin.horizontal, height = boxSize.y, }; using (new BackgroundColorScope(color)) { FusionEditorSkin.InlineBoxFullWidthStyle.Draw(boxRect, false, false, false, false); var labelRect = boxRect; labelRect = FusionEditorSkin.InlineBoxFullWidthStyle.padding.Remove(labelRect); FusionEditorSkin.RichLabelStyle.Draw(labelRect, content, false, false, false, false); if (drawSelector) { var selectorMargin = FusionEditorSkin.InlineSelectorStyle.margin; var selectorRect = new Rect() { x = selectorMargin.left, y = propertyRect.y - selectorMargin.top, width = propertyRect.x - selectorMargin.horizontal, height = propertyRect.height - boxSize.y - selectorMargin.bottom, }; if (hasFoldout) { selectorRect.width -= 20.0f; } FusionEditorSkin.InlineSelectorStyle.Draw(selectorRect, false, false, false, false); } } } propertyRect.height -= boxSize.y; return propertyRect; } } internal static void DrawScriptHeaderBackground(Rect position, Color color) { if (Event.current.type != EventType.Repaint) { return; } var style = FusionEditorSkin.ScriptHeaderBackgroundStyle; var boxMargin = style.margin; var boxRect = new Rect() { x = boxMargin.left, y = position.y - boxMargin.top, width = UnityInternal.EditorGUIUtility.contextWidth - boxMargin.horizontal, height = position.height + boxMargin.bottom, }; using (new BackgroundColorScope(color)) { style.Draw(boxRect, false, false, false, false); } } public static void DrawScriptHeaderIcon(Rect position) { if (Event.current.type != EventType.Repaint) { return; } var style = FusionEditorSkin.ScriptHeaderIconStyle; var boxMargin = style.margin; var boxRect = boxMargin.Remove(position); style.Draw(boxRect, false, false, false, false); } public static bool InjectScriptHeaderDrawer(Editor editor) => InjectScriptHeaderDrawer(editor, out _); internal static bool InjectScriptHeaderDrawer(Editor editor, out ScriptFieldDrawer drawer) => InjectScriptHeaderDrawer(editor.serializedObject, out drawer); public static bool InjectScriptHeaderDrawer(SerializedObject serializedObject) => InjectScriptHeaderDrawer(serializedObject, out _); internal static bool InjectScriptHeaderDrawer(SerializedObject serializedObject, out ScriptFieldDrawer drawer) { var sp = serializedObject.FindPropertyOrThrow(ScriptPropertyName); var rootType = serializedObject.targetObject.GetType(); var injected = TryInjectDrawer(sp, null, () => null, () => new ScriptFieldDrawer(), out drawer); if (drawer.attribute == null) { UnityInternal.PropertyDrawer.SetAttribute(drawer, rootType.GetCustomAttributes(true).SingleOrDefault() ?? new ScriptHelpAttribute()); } return injected; } public static void SetScriptFieldHidden(Editor editor, bool hidden) { var sp = editor.serializedObject.FindPropertyOrThrow(ScriptPropertyName); TryInjectDrawer(sp, null, () => null, () => new ScriptFieldDrawer(), out var drawer); drawer.ForceHide = hidden; } public static Rect LayoutHelpPrefix(Editor editor, SerializedProperty property) { var fieldInfo = UnityInternal.ScriptAttributeUtility.GetFieldInfoFromProperty(property, out _); if (fieldInfo == null) { return EditorGUILayout.GetControlRect(true); } var help = FusionCodeDoc.FindEntry(fieldInfo); return LayoutHelpPrefix(editor, property.propertyPath, help); } public static Rect LayoutHelpPrefix(Editor editor, MemberInfo memberInfo) { var help = FusionCodeDoc.FindEntry(memberInfo); return LayoutHelpPrefix(editor, memberInfo.Name, help); } public static Rect LayoutHelpPrefix(Editor editor, string path, GUIContent help) { var rect = EditorGUILayout.GetControlRect(true); if (help == null) { return rect; } var buttonRect = GetInlineHelpButtonRect(rect, false); var wasExpanded = IsHelpExpanded(editor, path); if (wasExpanded) { var helpSize = GetInlineBoxSize(help); var r = EditorGUILayout.GetControlRect(false, helpSize.y); r.y = rect.y; r.height += rect.height; DrawInlineBoxUnderProperty(help, r, FusionEditorSkin.HelpInlineBoxColor, true); } if (DrawInlineHelpButton(buttonRect, wasExpanded, doButton: true, doIcon: true)) { SetHelpExpanded(editor, path, !wasExpanded); } return rect; } private static void AddDrawer(SerializedProperty property, PropertyDrawer drawer) { var handler = UnityInternal.ScriptAttributeUtility.GetHandler(property); if (handler.m_PropertyDrawers == null) { handler.m_PropertyDrawers = new List(); } InsertPropertyDrawerByAttributeOrder(handler.m_PropertyDrawers, drawer); } private static bool TryInjectDrawer(SerializedProperty property, FieldInfo field, Func attributeFactory, Func drawerFactory, out DrawerType drawer) where DrawerType : PropertyDrawer { var handler = UnityInternal.ScriptAttributeUtility.GetHandler(property); drawer = GetPropertyDrawer(handler.m_PropertyDrawers); if (drawer != null) { return false; } if (handler.Equals(UnityInternal.ScriptAttributeUtility.sharedNullHandler)) { // need to add one? handler = UnityInternal.PropertyHandler.New(); UnityInternal.ScriptAttributeUtility.propertyHandlerCache.SetHandler(property, handler); } var attribute = attributeFactory(); drawer = drawerFactory(); FusionEditorLog.Assert(drawer != null, "drawer != null"); UnityInternal.PropertyDrawer.SetAttribute(drawer, attribute); UnityInternal.PropertyDrawer.SetFieldInfo(drawer, field); AddDrawer(property, drawer); return true; } internal static bool IsHelpExpanded(object id, string path) { return s_expandedHelp == (id, path); } internal static void SetHelpExpanded(object id, string path, bool value) { if (value) { s_expandedHelp = (id, path); } else { s_expandedHelp = default; } } private static bool HasPropertyDrawer(IEnumerable orderedDrawers) where T : PropertyDrawer { return orderedDrawers?.Any(x => x is T) ?? false; } private static T GetPropertyDrawer(IEnumerable orderedDrawers) where T : PropertyDrawer { return orderedDrawers?.OfType().FirstOrDefault(); } internal static int InsertPropertyDrawerByAttributeOrder(List orderedDrawers, T drawer) where T : PropertyDrawer { if (orderedDrawers == null) { throw new ArgumentNullException(nameof(orderedDrawers)); } if (drawer == null) { throw new ArgumentNullException(nameof(drawer)); } var index = orderedDrawers.BinarySearch(drawer, PropertyDrawerOrderComparer.Instance); if (index < 0) { index = ~index; } orderedDrawers.Insert(index, drawer); return index; } internal static class InlineHelpStyle { public const float MarginOuter = 16.0f; public static GUIContent HideInlineContent = new("", "Hide"); public static GUIContent ShowInlineContent = new("", ""); } internal static class LazyAuto { public static LazyAuto Create(Func valueFactory) { return new LazyAuto(valueFactory); } } internal class LazyAuto : Lazy { public LazyAuto(Func valueFactory) : base(valueFactory) { } public static implicit operator T(LazyAuto lazy) { return lazy.Value; } } private class PropertyDrawerOrderComparer : IComparer { public static readonly PropertyDrawerOrderComparer Instance = new(); public int Compare(PropertyDrawer x, PropertyDrawer y) { var ox = x.attribute?.order ?? int.MaxValue; var oy = y.attribute?.order ?? int.MaxValue; return ox - oy; } } } } #endregion #region FusionEditorGUI.Odin.cs namespace Fusion.Editor { using System; using UnityEditor; using UnityEngine; #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED using System.Collections.Generic; using Sirenix.Utilities.Editor; using Sirenix.OdinInspector.Editor; using Sirenix.Utilities; #endif public static partial class FusionEditorGUI { public static T IfOdin(T ifOdin, T ifNotOdin) { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED return ifOdin; #else return ifNotOdin; #endif } public static UnityEngine.Object ForwardObjectField(Rect position, UnityEngine.Object value, Type objectType, bool allowSceneObjects) { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED return SirenixEditorFields.UnityObjectField(position, value, objectType, allowSceneObjects); #else return EditorGUI.ObjectField(position, value, objectType, allowSceneObjects); #endif } public static UnityEngine.Object ForwardObjectField(Rect position, GUIContent label, UnityEngine.Object value, Type objectType, bool allowSceneObjects) { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED return SirenixEditorFields.UnityObjectField(position, label, value, objectType, allowSceneObjects); #else return EditorGUI.ObjectField(position, label, value, objectType, allowSceneObjects); #endif } public static bool ForwardPropertyField(Rect position, SerializedProperty property, GUIContent label, bool includeChildren, bool lastDrawer = true) { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED if (lastDrawer) { switch (property.propertyType) { case SerializedPropertyType.ObjectReference: { EditorGUI.BeginChangeCheck(); UnityInternal.ScriptAttributeUtility.GetFieldInfoFromProperty(property, out var fieldType); var value = SirenixEditorFields.UnityObjectField(position, label, property.objectReferenceValue, fieldType ?? typeof(UnityEngine.Object), true); if (EditorGUI.EndChangeCheck()) { property.objectReferenceValue = value; } return false; } case SerializedPropertyType.Integer: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.IntField(position, label, property.intValue); if (EditorGUI.EndChangeCheck()) { property.intValue = value; } return false; } case SerializedPropertyType.Float: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.FloatField(position, label, property.floatValue); if (EditorGUI.EndChangeCheck()) { property.floatValue = value; } return false; } case SerializedPropertyType.Color: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.ColorField(position, label, property.colorValue); if (EditorGUI.EndChangeCheck()) { property.colorValue = value; } return false; } case SerializedPropertyType.Vector2: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.Vector2Field(position, label, property.vector2Value); if (EditorGUI.EndChangeCheck()) { property.vector2Value = value; } return false; } case SerializedPropertyType.Vector3: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.Vector3Field(position, label, property.vector3Value); if (EditorGUI.EndChangeCheck()) { property.vector3Value = value; } return false; } case SerializedPropertyType.Vector4: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.Vector4Field(position, label, property.vector4Value); if (EditorGUI.EndChangeCheck()) { property.vector4Value = value; } return false; } case SerializedPropertyType.Quaternion: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.RotationField(position, label, property.quaternionValue, GlobalConfig.Instance.QuaternionDrawMode); if (EditorGUI.EndChangeCheck()) { property.quaternionValue = value; } return false; } case SerializedPropertyType.String: { EditorGUI.BeginChangeCheck(); var value = SirenixEditorFields.TextField(position, label, property.stringValue); if (EditorGUI.EndChangeCheck()) { property.stringValue = value; } return false; } case SerializedPropertyType.Enum: { UnityInternal.ScriptAttributeUtility.GetFieldInfoFromProperty(property, out var type); if (type != null && type.IsEnum) { EditorGUI.BeginChangeCheck(); bool flags = type.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0; Enum value = SirenixEditorFields.EnumDropdown(position, label, (Enum)Enum.ToObject(type, property.intValue)); if (EditorGUI.EndChangeCheck()) { property.intValue = Convert.ToInt32(Convert.ChangeType(value, Enum.GetUnderlyingType(type))); } return false; } break; } default: break; } } #endif return EditorGUI.PropertyField(position, property, label, includeChildren); } } } #endregion #region FusionEditorGUI.Scopes.cs namespace Fusion.Editor { using System; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; public static partial class FusionEditorGUI { public sealed class CustomEditorScope : IDisposable { private SerializedObject serializedObject; public bool HadChanges { get; private set; } public CustomEditorScope(SerializedObject so) { serializedObject = so; EditorGUI.BeginChangeCheck(); so.UpdateIfRequiredOrScript(); ScriptPropertyField(so); } public void Dispose() { HadChanges = EditorGUI.EndChangeCheck(); serializedObject.ApplyModifiedProperties(); } } public struct EnabledScope: IDisposable { private readonly bool value; public EnabledScope(bool enabled) { value = GUI.enabled; GUI.enabled = enabled; } public void Dispose() { GUI.enabled = value; } } public readonly struct BackgroundColorScope : IDisposable { private readonly Color value; public BackgroundColorScope(Color color) { value = GUI.backgroundColor; GUI.backgroundColor = color; } public void Dispose() { GUI.backgroundColor = value; } } public struct ColorScope: IDisposable { private readonly Color value; public ColorScope(Color color) { value = GUI.color; GUI.color = color; } public void Dispose() { GUI.color = value; } } public struct ContentColorScope: IDisposable { private readonly Color value; public ContentColorScope(Color color) { value = GUI.contentColor; GUI.contentColor = color; } public void Dispose() { GUI.contentColor = value; } } public struct FieldWidthScope: IDisposable { private readonly float value; public FieldWidthScope(float fieldWidth) { value = EditorGUIUtility.fieldWidth; EditorGUIUtility.fieldWidth = fieldWidth; } public void Dispose() { EditorGUIUtility.fieldWidth = value; } } public struct HierarchyModeScope: IDisposable { private readonly bool value; public HierarchyModeScope(bool value) { this.value = EditorGUIUtility.hierarchyMode; EditorGUIUtility.hierarchyMode = value; } public void Dispose() { EditorGUIUtility.hierarchyMode = value; } } public struct IndentLevelScope: IDisposable { private readonly int value; public IndentLevelScope(int indentLevel) { value = EditorGUI.indentLevel; EditorGUI.indentLevel = indentLevel; } public void Dispose() { EditorGUI.indentLevel = value; } } public struct LabelWidthScope: IDisposable { private readonly float value; public LabelWidthScope(float labelWidth) { value = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = labelWidth; } public void Dispose() { EditorGUIUtility.labelWidth = value; } } public struct ShowMixedValueScope: IDisposable { private readonly bool value; public ShowMixedValueScope(bool show) { value = EditorGUI.showMixedValue; EditorGUI.showMixedValue = show; } public void Dispose() { EditorGUI.showMixedValue = value; } } public struct PropertyScope : IDisposable { public PropertyScope(Rect position, GUIContent label, SerializedProperty property) { EditorGUI.BeginProperty(position, label, property); } public void Dispose() { EditorGUI.EndProperty(); } } public readonly struct PropertyScopeWithPrefixLabel : IDisposable { private readonly int indent; public PropertyScopeWithPrefixLabel(Rect position, GUIContent label, SerializedProperty property, out Rect indentedPosition) { EditorGUI.BeginProperty(position, label, property); indentedPosition = EditorGUI.PrefixLabel(position, label); indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; } public void Dispose() { EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } } public readonly struct BoxScope: IDisposable { private readonly int _indent; /// ///if fields include inline help (?) buttons, use indent : 1 /// public BoxScope(string message, int indent = 0, float space = 0.0f) { EditorGUILayout.BeginVertical(FusionEditorSkin.OutlineBoxStyle); if (!string.IsNullOrEmpty(message)) { EditorGUILayout.LabelField(message, EditorStyles.boldLabel); } if (space > 0.0f) { GUILayout.Space(space); } _indent = EditorGUI.indentLevel; if (indent != 0) { EditorGUI.indentLevel += indent; } } public void Dispose() { EditorGUI.indentLevel = _indent; EditorGUILayout.EndVertical(); } } public struct WarningScope: IDisposable { public WarningScope(string message, float space = 0.0f) { var backgroundColor = GUI.backgroundColor; GUI.backgroundColor = FusionEditorSkin.WarningInlineBoxColor; EditorGUILayout.BeginVertical(FusionEditorSkin.InlineBoxFullWidthScopeStyle); GUI.backgroundColor = backgroundColor; EditorGUILayout.LabelField(new GUIContent(message, FusionEditorSkin.WarningIcon), FusionEditorSkin.RichLabelStyle); if (space > 0.0f) { GUILayout.Space(space); } } public void Dispose() { EditorGUILayout.EndVertical(); } } public struct ErrorScope : IDisposable { public ErrorScope(string message, float space = 0.0f) { var backgroundColor = GUI.backgroundColor; GUI.backgroundColor = FusionEditorSkin.ErrorInlineBoxColor; EditorGUILayout.BeginVertical(FusionEditorSkin.InlineBoxFullWidthScopeStyle); GUI.backgroundColor = backgroundColor; EditorGUILayout.LabelField(new GUIContent(message, FusionEditorSkin.ErrorIcon), FusionEditorSkin.RichLabelStyle); if (space > 0.0f) { GUILayout.Space(space); } } public void Dispose() { EditorGUILayout.EndVertical(); } } public readonly struct GUIContentScope : IDisposable { private readonly string _text; private readonly string _tooltip; private readonly GUIContent _content; public GUIContentScope(GUIContent content) { _content = content; _text = content?.text; _tooltip = content?.tooltip; } public void Dispose() { if (_content != null) { _content.text = _text; _content.tooltip = _tooltip; } } } } } #endregion #region FusionEditorGUI.Utils.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; public static partial class FusionEditorGUI { public const string ScriptPropertyName = "m_Script"; private const int IconHeight = 14; public static readonly GUIContent WhitespaceContent = new(" "); public static Color PrefebOverridenColor => new(1f / 255f, 153f / 255f, 235f / 255f, 0.75f); public static float FoldoutWidth => 16.0f; public static Rect Decorate(Rect rect, string tooltip, MessageType messageType, bool hasLabel = false, bool drawBorder = true, bool drawButton = true) { if (hasLabel) { rect.xMin += EditorGUIUtility.labelWidth; } var content = EditorGUIUtility.TrTextContentWithIcon(string.Empty, tooltip, messageType); var iconRect = rect; iconRect.width = Mathf.Min(16, rect.width); iconRect.xMin -= iconRect.width; iconRect.y += (iconRect.height - IconHeight) / 2; iconRect.height = IconHeight; if (drawButton) { using (new EnabledScope(true)) { GUI.Label(iconRect, content, GUIStyle.none); } } //GUI.Label(iconRect, content, new GUIStyle()); if (drawBorder) { Color borderColor; switch (messageType) { case MessageType.Warning: borderColor = new Color(1.0f, 0.5f, 0.0f); break; case MessageType.Error: borderColor = new Color(1.0f, 0.0f, 0.0f); break; default: borderColor = new Color(1f, 1f, 1f, .0f); break; } GUI.DrawTexture(rect, Texture2D.whiteTexture, ScaleMode.StretchToFill, false, 0, borderColor, 1.0f, 1.0f); } return iconRect; } public static void AppendTooltip(string tooltip, ref GUIContent label) { if (!string.IsNullOrEmpty(tooltip)) { label = new GUIContent(label); if (string.IsNullOrEmpty(label.tooltip)) { label.tooltip = tooltip; } else { label.tooltip += "\n\n" + tooltip; } } } public static void ScriptPropertyField(Editor editor) { ScriptPropertyField(editor.serializedObject); } public static void ScriptPropertyField(SerializedObject obj) { var scriptProperty = obj.FindProperty(ScriptPropertyName); if (scriptProperty != null) { using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.PropertyField(scriptProperty); } } } public static void Overlay(Rect position, string label) { GUI.Label(position, label, FusionEditorSkin.OverlayLabelStyle); } public static void Overlay(Rect position, GUIContent label) { GUI.Label(position, label, FusionEditorSkin.OverlayLabelStyle); } public static float GetLinesHeight(int count) { return count * (EditorGUIUtility.singleLineHeight) + (count - 1) * EditorGUIUtility.standardVerticalSpacing; } public static float GetLinesHeightWithNarrowModeSupport(int count) { if (!EditorGUIUtility.wideMode) { count++; } return count * (EditorGUIUtility.singleLineHeight) + (count - 1) * EditorGUIUtility.standardVerticalSpacing; } public static System.Type GetDrawerTypeIncludingWorkarounds(System.Attribute attribute) { var drawerType = UnityInternal.ScriptAttributeUtility.GetDrawerTypeForType(attribute.GetType(), null, false); if (drawerType == null) { return null; } if (drawerType == typeof(PropertyDrawerForArrayWorkaround)) { drawerType = PropertyDrawerForArrayWorkaround.GetDrawerType(attribute.GetType()); } return drawerType; } public static void DisplayTypePickerMenu(Rect position, Type[] baseTypes, Action callback, Func filter, string noneOptionLabel = "[None]", bool groupByNamespace = true, Type selectedType = null) { var types = new List(); foreach (var baseType in baseTypes) { types.AddRange(TypeCache.GetTypesDerivedFrom(baseType).Where(filter)); } if (baseTypes.Length > 1) { types = types.Distinct().ToList(); } types.Sort((a, b) => string.CompareOrdinal(a.FullName, b.FullName)); List menuOptions = new List(); var actualTypes = new Dictionary(); menuOptions.Add(new GUIContent(noneOptionLabel)); actualTypes.Add(noneOptionLabel, null); int selectedIndex = -1; foreach (var ns in types.GroupBy(x => string.IsNullOrEmpty(x.Namespace) ? "[Global Namespace]" : x.Namespace)) { foreach (var t in ns) { var nameWithoutNamespace = t.FullName; if (string.IsNullOrEmpty(nameWithoutNamespace)) { continue; } if (t.Namespace != null && t.Namespace.Length > 0) { nameWithoutNamespace = nameWithoutNamespace.Substring(t.Namespace.Length + 1); } string path; if (groupByNamespace) { path = ns.Key + "/" + nameWithoutNamespace; } else { path = t.FullName; } if (actualTypes.ContainsKey(path)) { continue; } menuOptions.Add(new GUIContent(path)); actualTypes.Add(path, t); if (selectedType == t) { selectedIndex = menuOptions.Count - 1; } } } EditorUtility.DisplayCustomMenu(position, menuOptions.ToArray(), selectedIndex, (userData, options, selected) => { var path = options[selected]; var newType = ((Dictionary)userData)[path]; callback(newType); }, actualTypes); } public static void DisplayTypePickerMenu(Rect position, Type[] baseTypes, Action callback, string noneOptionLabel = "[None]", bool groupByNamespace = true, Type selectedType = null, bool enableAbstract = false, bool enableGenericTypeDefinitions = false) { DisplayTypePickerMenu(position, baseTypes, callback, x => (enableAbstract || !x.IsAbstract) && (enableGenericTypeDefinitions || !x.IsGenericTypeDefinition)); } public static void DisplayTypePickerMenu(Rect position, Type baseType, Action callback, string noneOptionLabel = "[None]", bool groupByNamespace = true, Type selectedType = null, bool enableAbstract = false, bool enableGenericTypeDefinitions = false) { DisplayTypePickerMenu(position, new [] { baseType }, callback, x => (enableAbstract || !x.IsAbstract) && (enableGenericTypeDefinitions || !x.IsGenericTypeDefinition)); } } } #endregion #region FusionEditorUtility.cs namespace Fusion.Editor { using System; using UnityEditor; partial class FusionEditorUtility { public static void DelayCall(EditorApplication.CallbackFunction callback) { FusionEditorLog.Assert(callback.Target == null, "DelayCall callback needs to stateless"); EditorApplication.delayCall -= callback; EditorApplication.delayCall += callback; } } } #endregion #region FusionGlobalScriptableObjectEditorAttribute.cs namespace Fusion.Editor { using System; using UnityEditor; class FusionGlobalScriptableObjectEditorAttribute : FusionGlobalScriptableObjectSourceAttribute { public FusionGlobalScriptableObjectEditorAttribute(Type objectType) : base(objectType) { } public override FusionGlobalScriptableObjectLoadResult Load(Type type) { var defaultAssetPath = FusionGlobalScriptableObjectUtils.FindDefaultAssetPath(type, fallbackToSearchWithoutLabel: true); if (string.IsNullOrEmpty(defaultAssetPath)) { return default; } var result = (FusionGlobalScriptableObject)AssetDatabase.LoadAssetAtPath(defaultAssetPath, type); FusionEditorLog.Assert(result); return result; } } } #endregion #region FusionGlobalScriptableObjectUtils.cs namespace Fusion.Editor { using System; using System.IO; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; public static class FusionGlobalScriptableObjectUtils { public const string GlobalAssetLabel = "FusionDefaultGlobal"; public static void SetDirty(this FusionGlobalScriptableObject obj) { EditorUtility.SetDirty(obj); } public static string GetGlobalAssetPath() where T : FusionGlobalScriptableObject { return FindDefaultAssetPath(typeof(T), fallbackToSearchWithoutLabel: false); } public static bool TryGetGlobalAssetPath(out string path) where T : FusionGlobalScriptableObject { path = FindDefaultAssetPath(typeof(T), fallbackToSearchWithoutLabel: false); return !string.IsNullOrEmpty(path); } private static FusionGlobalScriptableObjectAttribute GetAttributeOrThrow(Type type) { var attribute = type.GetCustomAttribute(); if (attribute == null) { throw new InvalidOperationException($"Type {type.FullName} needs to be decorated with {nameof(FusionGlobalScriptableObjectAttribute)}"); } return attribute; } public static bool EnsureAssetExists() where T : FusionGlobalScriptableObject { var defaultAssetPath = FindDefaultAssetPath(typeof(T), fallbackToSearchWithoutLabel: true); if (!string.IsNullOrEmpty(defaultAssetPath)) { // already exists return false; } // need to create a new asset CreateDefaultAsset(typeof(T)); return true; } private static FusionGlobalScriptableObject CreateDefaultAsset(Type type) { var attribute = GetAttributeOrThrow(type); var directoryPath = Path.GetDirectoryName(attribute.DefaultPath); if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); AssetDatabase.Refresh(); } if (File.Exists(attribute.DefaultPath)) { throw new InvalidOperationException($"Asset file already exists at '{attribute.DefaultPath}'"); } // is this a regular asset? if (attribute.DefaultPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) { var instance = (FusionGlobalScriptableObject)ScriptableObject.CreateInstance(type); AssetDatabase.CreateAsset(instance, attribute.DefaultPath); AssetDatabase.SaveAssets(); SetGlobal(instance); EditorUtility.SetDirty(instance); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); FusionEditorLog.TraceImport($"Created new global {type.Name} instance at {attribute.DefaultPath}"); return instance; } else { string defaultContents = null; if (!string.IsNullOrEmpty(attribute.DefaultContentsGeneratorMethod)) { var method = type.GetMethod(attribute.DefaultContentsGeneratorMethod, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); if (method == null) { throw new InvalidOperationException($"Generator method '{attribute.DefaultContentsGeneratorMethod}' not found on type {type.FullName}"); } defaultContents = (string)method.Invoke(null, null); } if (defaultContents == null) { defaultContents = attribute.DefaultContents; } File.WriteAllText(attribute.DefaultPath, defaultContents ?? string.Empty); AssetDatabase.ImportAsset(attribute.DefaultPath, ImportAssetOptions.ForceUpdate); var instance = (FusionGlobalScriptableObject)AssetDatabase.LoadAssetAtPath(attribute.DefaultPath, type); if (!instance) { throw new InvalidOperationException($"Failed to load a newly created asset at '{attribute.DefaultPath}'"); } SetGlobal(instance); FusionEditorLog.TraceImport($"Created new global {type.Name} instance at {attribute.DefaultPath}"); return instance; } } private static bool IsDefault(this FusionGlobalScriptableObject obj) { return Array.IndexOf(AssetDatabase.GetLabels(obj), GlobalAssetLabel) >= 0; } private static bool SetGlobal(FusionGlobalScriptableObject obj) { var labels = AssetDatabase.GetLabels(obj); if (Array.IndexOf(labels, GlobalAssetLabel) >= 0) { return false; } Array.Resize(ref labels, labels.Length + 1); labels[^1] = GlobalAssetLabel; AssetDatabase.SetLabels(obj, labels); return true; } internal static string FindDefaultAssetPath(Type type, bool fallbackToSearchWithoutLabel = false) { // first try to locate assets of the type, with the default label var defaults = AssetDatabaseUtils.IterateAssets(type: type, label: GlobalAssetLabel) .Select(x => AssetDatabase.GUIDToAssetPath(x.guid)) .ToList(); if (defaults.Count == 1) { return defaults[0]; } if (defaults.Count > 1) { throw new InvalidOperationException($"There are multiple assets of type '{type.Name}' marked as default: '{string.Join("', '", defaults)}'. Remove all labels but one."); } if (fallbackToSearchWithoutLabel) { // now as a fallback, locate all assets of the type and apply the label only if there is only one var candidates = AssetDatabaseUtils.IterateAssets(type: type) .Select(x => AssetDatabase.GUIDToAssetPath(x.guid)) .ToList(); if (candidates.Count == 1) { // mark as default AssetDatabaseUtils.SetLabel(candidates[0], GlobalAssetLabel, true); EditorUtility.SetDirty(AssetDatabase.LoadMainAssetAtPath(candidates[0])); FusionEditorLog.Log($"Set '{candidates[0]}' as the default asset for '{type.Name}'"); return candidates[0]; } if (candidates.Count > 1) { throw new InvalidOperationException($"There are no assets of type '{type.Name}' with {GlobalAssetLabel}, but there are multiple candidates: '{string.Join("', '", candidates)}'. Assign label manually or remove all but one."); } } // no candidates return string.Empty; } public static bool TryImportGlobal() where T : FusionGlobalScriptableObject { var globalPath = GetGlobalAssetPath(); if (string.IsNullOrEmpty(globalPath)) { return false; } AssetDatabase.ImportAsset(globalPath); return true; } } } #endregion #region FusionGrid.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; using Object = UnityEngine.Object; [Serializable] public class FusionGridState : TreeViewState { public MultiColumnHeaderState HeaderState; public bool SyncSelection; } public class FusionGridItem : TreeViewItem { public virtual Object TargetObject => null; } public abstract class FusionGrid : FusionGrid where TItem : FusionGridItem { } [Serializable] public abstract class FusionGrid where TState : FusionGridState, new() where TItem : FusionGridItem { [SerializeField] public bool HasValidState; [SerializeField] public TState State; [SerializeField] public float UpdatePeriod = 1.0f; class GUIState { public InternalTreeView TreeView; public MultiColumnHeader MultiColumnHeader; public SearchField SearchField; } [NonSerialized] private Lazy _gui; [NonSerialized] private Lazy _columns; [NonSerialized] private float _nextUpdateTime; [NonSerialized] private int _lastContentHash; public virtual int GetContentHash() { return 0; } public FusionGrid() { ResetColumns(); ResetGUI(); } void ResetColumns() { _columns = new Lazy(() => { var columns = CreateColumns().ToArray(); for (int i = 0; i < columns.Length; ++i) { ((MultiColumnHeaderState.Column)columns[i]).userData = i; } return columns; }); } void ResetGUI() { _gui = new Lazy(() => { var result = new GUIState(); result.MultiColumnHeader = new MultiColumnHeader(State.HeaderState); result.MultiColumnHeader.sortingChanged += _ => result.TreeView.Reload(); result.MultiColumnHeader.ResizeToFit(); result.SearchField = new SearchField(); result.SearchField.downOrUpArrowKeyPressed += () => result.TreeView.SetFocusAndEnsureSelectedItem(); result.TreeView = new InternalTreeView(this, result.MultiColumnHeader); return result; }); } public void OnInspectorUpdate() { if (!HasValidState) { return; } if (!_gui.IsValueCreated) { return; } if (_nextUpdateTime > Time.realtimeSinceStartup) { return; } _nextUpdateTime = Time.realtimeSinceStartup + UpdatePeriod; var hash = GetContentHash(); if (_lastContentHash == hash) { return; } _lastContentHash = hash; _gui.Value.TreeView.Reload(); } public void OnEnable() { if (HasValidState) { return; } var visibleColumns = new List(); int sortingColumn = -1; for (int i = 0; i < _columns.Value.Length; ++i) { var column = _columns.Value[i]; if (!column.initiallyVisible) { continue; } visibleColumns.Add(i); if (sortingColumn < 0 && column.initiallySorted) { sortingColumn = i; column.sortedAscending = true; } } var headerState = new MultiColumnHeaderState(_columns.Value.Cast().ToArray()) { visibleColumns = visibleColumns.ToArray(), sortedColumnIndex = sortingColumn, }; State = new TState() { HeaderState = headerState }; HasValidState = true; ResetGUI(); } public void OnGUI(Rect rect) { _gui.Value.TreeView.OnGUI(rect); } public void DrawToolbarReloadButton() { if (GUILayout.Button(new GUIContent(FusionEditorSkin.RefreshIcon, "Refresh"), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) { _gui.Value.TreeView.Reload(); } } public void DrawToolbarSyncSelectionButton() { EditorGUI.BeginChangeCheck(); State.SyncSelection = GUILayout.Toggle(State.SyncSelection, "Sync Selection", EditorStyles.toolbarButton); if (EditorGUI.EndChangeCheck()) { if (State.SyncSelection) { _gui.Value.TreeView.SyncSelection(); } } } public void DrawToolbarSearchField() { _gui.Value.TreeView.searchString = _gui.Value.SearchField.OnToolbarGUI(_gui.Value.TreeView.searchString); } public void DrawToolbarResetView() { if (GUILayout.Button("Reset View", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) { HasValidState = false; ResetColumns(); } } public void ResetTree() { ResetGUI(); } protected abstract IEnumerable CreateColumns(); protected abstract IEnumerable CreateRows(); protected virtual GenericMenu CreateContextMenu(TItem item, TreeView treeView) { return null; } protected static Column MakeSimpleColumn(Expression> propertyExpression, Column column) { string propertyName; if (propertyExpression.Body is MemberExpression memberExpression) { propertyName = memberExpression.Member.Name; } else { throw new ArgumentException("Expression is not a member access expression."); } var accessor = propertyExpression.Compile(); Func toString = item => $"{accessor(item)}"; column.getSearchText ??= toString; column.getComparer ??= order => (a, b) => EditorUtility.NaturalCompare(toString(a), toString(b)) * order; column.cellGUI ??= (item, rect, selected, focused) => TreeView.DefaultGUI.Label(rect, toString(item), selected, focused); if (string.IsNullOrEmpty(column.headerContent.text) && string.IsNullOrEmpty(column.headerContent.tooltip)) { column.headerContent = new GUIContent(propertyName); } return column; } public class Column : MultiColumnHeaderState.Column { public Func getSearchText; public Func> getComparer; public Action cellGUI; public bool initiallyVisible = true; public bool initiallySorted; // // [Obsolete("Do not use", true)] // public new int userData => throw new NotImplementedException(); } class InternalTreeView : TreeView { public InternalTreeView(FusionGrid grid, MultiColumnHeader header) : base(grid.State, header) { Grid = grid; showAlternatingRowBackgrounds = true; this.Reload(); } public new TState state => (TState)base.state; public FusionGrid Grid { get; } protected override void SelectionChanged(IList selectedIds) { base.SelectionChanged(selectedIds); if (state.SyncSelection) { SyncSelection(); } } protected override void SingleClickedItem(int id) { var item = (TItem)FindItem(id, rootItem); var obj = item.TargetObject; if (obj) { EditorGUIUtility.PingObject(obj); } base.SingleClickedItem(id); } public void SyncSelection() { List selection = new List(); foreach (var id in this.state.selectedIDs) { if (id == 0) { continue; } var item = (TItem)FindItem(id, rootItem); var obj = item.TargetObject; if (obj) { selection.Add(obj); } } Selection.objects = selection.ToArray(); } private Column GetColumnForIndex(int index) { var column = multiColumnHeader.GetColumn(index); var ud = column.userData; return Grid._columns.Value[ud]; } protected override TreeViewItem BuildRoot() { var allItems = new List(); var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" }; foreach (var row in Grid.CreateRows()) { allItems.Add(row); } SetupParentsAndChildrenFromDepths(root, allItems.Cast().ToList()); return root; } private class ComparisonComparer : IComparer { public Comparison Comparison; public int Compare(TItem x, TItem y) => Comparison(x, y); } private Comparison GetComparision() { if (multiColumnHeader.sortedColumnIndex < 0) { return null; } var column = GetColumnForIndex(multiColumnHeader.sortedColumnIndex); var isSortedAscending = multiColumnHeader.IsSortedAscending(multiColumnHeader.sortedColumnIndex); return column.getComparer(isSortedAscending ? 1 : -1); } protected override IList BuildRows(TreeViewItem root) { var comparision = GetComparision(); if (comparision == null) { return base.BuildRows(root); } // stable sort return base.BuildRows(root).OrderBy(x => (TItem)x, new ComparisonComparer() { Comparison = comparision }).ToArray(); } protected override void ContextClickedItem(int id) { var item = (TItem)FindItem(id, rootItem); if (item == null) { return; } var menu = Grid.CreateContextMenu(item, this); if (menu != null) { menu.ShowAsContext(); } } protected override void RowGUI(RowGUIArgs args) { for (var i = 0; i < args.GetNumVisibleColumns(); ++i) { var cellRect = args.GetCellRect(i); CenterRectUsingSingleLineHeight(ref cellRect); var item = (TItem)args.item; var column = GetColumnForIndex(args.GetColumn(i)); column.cellGUI?.Invoke(item, cellRect, args.selected, args.focused); } } protected override bool DoesItemMatchSearch(TreeViewItem item_, string search) { var item = item_ as TItem; if (item == null) { return base.DoesItemMatchSearch(item_, search); } var searchParts = (search ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries); if (searchParts.Length == 0) { return true; } var columns = multiColumnHeader.state.columns; for (var i = 0; i < columns.Length; ++i) { if (!multiColumnHeader.IsColumnVisible(i)) { continue; } var column = GetColumnForIndex(i); var text = column.getSearchText?.Invoke(item); if (text == null) { continue; } bool columnMatchesSearch = true; foreach (var part in searchParts) { if (!text.Contains(part, StringComparison.OrdinalIgnoreCase)) { columnMatchesSearch = false; break; } } if (columnMatchesSearch) { return true; } } return false; } } class InternalTreeViewItem : TreeViewItem { } } } #endregion #region FusionMonoBehaviourDefaultEditor.cs namespace Fusion.Editor { using UnityEditor; [CustomEditor(typeof(FusionMonoBehaviour), true)] internal class FusionMonoBehaviourDefaultEditor : FusionEditor { } } #endregion #region FusionPropertyDrawerMetaAttribute.cs namespace Fusion.Editor { using System; [AttributeUsage(AttributeTargets.Class)] public class FusionPropertyDrawerMetaAttribute : Attribute { public bool HasFoldout { get; set; } public bool HandlesUnits { get; set; } } } #endregion #region FusionScriptableObjectDefaultEditor.cs namespace Fusion.Editor { using UnityEditor; [CustomEditor(typeof(FusionScriptableObject), true)] internal class FusionScriptableObjectDefaultEditor : FusionEditor { } } #endregion #region RawDataDrawer.cs namespace Fusion.Editor { using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEngine; public struct RawDataDrawer { private StringBuilder _builder; private GUIContent _lastValue; private int _lastHash; public void Clear() { _builder?.Clear(); _lastHash = 0; _lastValue = GUIContent.none; } public bool HasContent => _lastValue != null && _lastValue.text.Length > 0; public unsafe void Refresh(Span data, int maxLength = 2048, bool addSpaces = true) where T : unmanaged { int charactersPerElement = 2 * sizeof(T); int arrayHash = 0; int effectiveArraySize; { int length = 0; int i; for (i = 0; i < data.Length && length < maxLength; ++i) { arrayHash = arrayHash * 31 + data[i].GetHashCode(); length += charactersPerElement; if (addSpaces) { length += 1; } } effectiveArraySize = i; } if (_builder == null || arrayHash != _lastHash) { var format = "{0:x" + charactersPerElement + "}" + (addSpaces ? " " : ""); _builder ??= new StringBuilder(); _builder.Clear(); for (int i = 0; i < effectiveArraySize; ++i) { _builder.AppendFormat(format, data[i]); } if (effectiveArraySize < data.Length) { _builder.AppendLine("..."); } _lastHash = arrayHash; _lastValue = new GUIContent(_builder.ToString()); } else { Debug.Assert(_lastValue != null); } } public void Refresh(IList values, int maxLength = 2048) { Assert.Check(values != null); const int charactersPerElement = 2; int arraySize = values.Count; int arrayHash = 0; int effectiveArraySize; { int length = 0; int i; for (i = 0; i < arraySize && length < maxLength; ++i) { arrayHash = arrayHash * 31 + values[i]; length += charactersPerElement + 1; } effectiveArraySize = i; } if (_builder == null || arrayHash != _lastHash) { var format = "{0:x" + charactersPerElement + "} "; _builder ??= new StringBuilder(); _builder.Clear(); for (int i = 0; i < effectiveArraySize; ++i) { _builder.AppendFormat(format, values[i]); } if (effectiveArraySize < arraySize) { _builder.AppendLine("..."); } _lastHash = arrayHash; _lastValue = new GUIContent(_builder.ToString()); } else { Debug.Assert(_lastValue != null); } } public void Refresh(SerializedProperty property, int maxLength = 2048) { Assert.Check(property != null); Assert.Check(property.isArray); int charactersPerElement; switch (property.arrayElementType) { case "long": case "ulong": charactersPerElement = 16; break; case "int": case "uint": charactersPerElement = 8; break; case "short": case "ushort": charactersPerElement = 4; break; case "sbyte": case "byte": charactersPerElement = 2; break; default: throw new NotImplementedException(property.arrayElementType); } int arrayHash = 0; int effectiveArraySize; { int length = 0; int i; for (i = 0; i < property.arraySize && length < maxLength; ++i) { arrayHash = arrayHash * 31 + property.GetArrayElementAtIndex(i).longValue.GetHashCode(); length += charactersPerElement + 1; } effectiveArraySize = i; } if (_builder == null || arrayHash != _lastHash) { var format = "{0:x" + charactersPerElement + "} "; _builder ??= new StringBuilder(); _builder.Clear(); for (int i = 0; i < effectiveArraySize; ++i) { _builder.AppendFormat(format, property.GetArrayElementAtIndex(i).longValue); } if (effectiveArraySize < property.arraySize) { _builder.AppendLine("..."); } _lastHash = arrayHash; _lastValue = new GUIContent(_builder.ToString()); } else { Debug.Assert(_lastValue != null); } } public float GetHeight(float width) { return FusionEditorSkin.RawDataStyle.Value.CalcHeight(_lastValue ?? GUIContent.none, width); } public string Draw(Rect position) => Draw(GUIContent.none, position); public string Draw(GUIContent label, Rect position) { var id = GUIUtility.GetControlID(UnityInternal.EditorGUI.DelayedTextFieldHash, FocusType.Keyboard, position); return UnityInternal.EditorGUI.DelayedTextFieldInternal(position, id, label, _lastValue.text ?? string.Empty, "0123456789abcdefABCDEF ", FusionEditorSkin.RawDataStyle); } public string DrawLayout() { var position = EditorGUILayout.GetControlRect(false, 18f, FusionEditorSkin.RawDataStyle); return Draw(position); } } } #endregion #region ReflectionUtils.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using UnityEditor; static partial class ReflectionUtils { public const BindingFlags DefaultBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; public static Type GetUnityLeafType(this Type type) { if (type.HasElementType) { type = type.GetElementType(); } else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { type = type.GetGenericArguments()[0]; } return type; } public static T CreateMethodDelegate(this Type type, string methodName, BindingFlags flags = DefaultBindingFlags) where T : Delegate { try { return CreateMethodDelegateInternal(type, methodName, flags); } catch (Exception ex) { throw new InvalidOperationException(CreateMethodExceptionMessage(type.Assembly, type.FullName, methodName, flags), ex); } } public static Delegate CreateMethodDelegate(this Type type, string methodName, BindingFlags flags, Type delegateType) { try { return CreateMethodDelegateInternal(type, methodName, flags, delegateType); } catch (Exception ex) { throw new InvalidOperationException(CreateMethodExceptionMessage(type.Assembly, type.FullName, methodName, flags, delegateType), ex); } } public static T CreateMethodDelegate(Assembly assembly, string typeName, string methodName, BindingFlags flags = DefaultBindingFlags) where T : Delegate { try { var type = assembly.GetType(typeName, true); return CreateMethodDelegateInternal(type, methodName, flags); } catch (Exception ex) { throw new InvalidOperationException(CreateMethodExceptionMessage(assembly, typeName, methodName, flags), ex); } } public static Delegate CreateMethodDelegate(Assembly assembly, string typeName, string methodName, BindingFlags flags, Type delegateType) { try { var type = assembly.GetType(typeName, true); return CreateMethodDelegateInternal(type, methodName, flags, delegateType); } catch (Exception ex) { throw new InvalidOperationException(CreateMethodExceptionMessage(assembly, typeName, methodName, flags, delegateType), ex); } } public static T CreateMethodDelegate(this Type type, string methodName, BindingFlags flags, Type delegateType, params DelegateSwizzle[] fallbackSwizzles) where T : Delegate { try { var method = GetMethodOrThrow(type, methodName, flags, delegateType, fallbackSwizzles, out var swizzle); var delegateParameters = typeof(T).GetMethod("Invoke").GetParameters(); var parameters = new List(); for (var i = 0; i < delegateParameters.Length; ++i) { parameters.Add(Expression.Parameter(delegateParameters[i].ParameterType, $"param_{i}")); } var convertedParameters = new List(); { var methodParameters = method.GetParameters(); if (swizzle == null) { for (int i = 0, j = method.IsStatic ? 0 : 1; i < methodParameters.Length; ++i, ++j) { convertedParameters.Add(Expression.Convert(parameters[j], methodParameters[i].ParameterType)); } } else { var swizzledParameters = swizzle.Swizzle(parameters.ToArray()); for (int i = 0, j = method.IsStatic ? 0 : 1; i < methodParameters.Length; ++i, ++j) { convertedParameters.Add(Expression.Convert(swizzledParameters[j], methodParameters[i].ParameterType)); } } } MethodCallExpression callExpression; if (method.IsStatic) { callExpression = Expression.Call(method, convertedParameters); } else { var instance = Expression.Convert(parameters[0], method.DeclaringType); callExpression = Expression.Call(instance, method, convertedParameters); } var l = Expression.Lambda(typeof(T), callExpression, parameters); var del = l.Compile(); return (T)del; } catch (Exception ex) { throw new InvalidOperationException(CreateMethodExceptionMessage(type.Assembly, type.FullName, methodName, flags), ex); } } public static T CreateConstructorDelegate(this Type type, BindingFlags flags, Type delegateType, params DelegateSwizzle[] fallbackSwizzles) where T : Delegate { try { var constructor = GetConstructorOrThrow(type, flags, delegateType, fallbackSwizzles, out var swizzle); var delegateParameters = typeof(T).GetMethod("Invoke").GetParameters(); var parameters = new List(); for (var i = 0; i < delegateParameters.Length; ++i) { parameters.Add(Expression.Parameter(delegateParameters[i].ParameterType, $"param_{i}")); } var convertedParameters = new List(); { var constructorParameters = constructor.GetParameters(); if (swizzle == null) { for (int i = 0, j = 0; i < constructorParameters.Length; ++i, ++j) { convertedParameters.Add(Expression.Convert(parameters[j], constructorParameters[i].ParameterType)); } } else { var swizzledParameters = swizzle.Swizzle(parameters.ToArray()); for (int i = 0, j = 0; i < constructorParameters.Length; ++i, ++j) { convertedParameters.Add(Expression.Convert(swizzledParameters[j], constructorParameters[i].ParameterType)); } } } var newExpression = Expression.New(constructor, convertedParameters); var l = Expression.Lambda(typeof(T), newExpression, parameters); var del = l.Compile(); return (T)del; } catch (Exception ex) { throw new InvalidOperationException(CreateConstructorExceptionMessage(type.Assembly, type.FullName, flags), ex); } } /// /// Returns the first found member of the given name. Includes private members. /// public static MemberInfo GetMemberIncludingBaseTypes(this Type type, string memberName, BindingFlags flags = DefaultBindingFlags, Type stopAtType = null) { var members = type.GetMember(memberName, flags); if (members.Length > 0) { return members[0]; } type = type.BaseType; // loop as long as we have a parent class to search. while (type != null) { // No point recursing into the abstracts. if (type == stopAtType) { break; } members = type.GetMember(memberName, flags); if (members.Length > 0) { return members[0]; } type = type.BaseType; } return null; } /// /// Normal reflection GetField() won't find private fields in parents (only will find protected). So this recurses the /// hard to find privates. /// This is needed since Unity serialization does find inherited privates. /// public static FieldInfo GetFieldIncludingBaseTypes(this Type type, string fieldName, BindingFlags flags = DefaultBindingFlags, Type stopAtType = null) { var field = type.GetField(fieldName, flags); if (field != null) { return field; } type = type.BaseType; // loop as long as we have a parent class to search. while (type != null) { // No point recursing into the abstracts. if (type == stopAtType) { break; } field = type.GetField(fieldName, flags); if (field != null) { return field; } type = type.BaseType; } return null; } public static FieldInfo GetFieldOrThrow(this Type type, string fieldName, BindingFlags flags = DefaultBindingFlags) { var field = type.GetField(fieldName, flags); if (field == null) { throw new ArgumentOutOfRangeException(nameof(fieldName), CreateFieldExceptionMessage(type.Assembly, type.FullName, fieldName, flags)); } return field; } public static FieldInfo GetFieldOrThrow(this Type type, string fieldName, BindingFlags flags = DefaultBindingFlags) { return GetFieldOrThrow(type, fieldName, typeof(T), flags); } public static FieldInfo GetFieldOrThrow(this Type type, string fieldName, Type fieldType, BindingFlags flags = DefaultBindingFlags) { var field = type.GetField(fieldName, flags); if (field == null) { throw new ArgumentOutOfRangeException(nameof(fieldName), CreateFieldExceptionMessage(type.Assembly, type.FullName, fieldName, flags)); } if (fieldType != null) { if (field.FieldType != fieldType) { throw new InvalidProgramException($"Field {type.FullName}.{fieldName} is of type {field.FieldType}, not expected {fieldType}"); } } return field; } public static PropertyInfo GetPropertyOrThrow(this Type type, string propertyName, BindingFlags flags = DefaultBindingFlags) { return GetPropertyOrThrow(type, propertyName, typeof(T), flags); } public static PropertyInfo GetPropertyOrThrow(this Type type, string propertyName, Type propertyType, BindingFlags flags = DefaultBindingFlags) { var property = type.GetProperty(propertyName, flags); if (property == null) { throw new ArgumentOutOfRangeException(nameof(propertyName), CreateFieldExceptionMessage(type.Assembly, type.FullName, propertyName, flags)); } if (property.PropertyType != propertyType) { throw new InvalidProgramException($"Property {type.FullName}.{propertyName} is of type {property.PropertyType}, not expected {propertyType}"); } return property; } public static PropertyInfo GetPropertyOrThrow(this Type type, string propertyName, BindingFlags flags = DefaultBindingFlags) { var property = type.GetProperty(propertyName, flags); if (property == null) { throw new ArgumentOutOfRangeException(nameof(propertyName), CreateFieldExceptionMessage(type.Assembly, type.FullName, propertyName, flags)); } return property; } public static ConstructorInfo GetConstructorInfoOrThrow(this Type type, Type[] types, BindingFlags flags = DefaultBindingFlags) { var constructor = type.GetConstructor(flags, null, types, null); if (constructor == null) { throw new ArgumentOutOfRangeException(nameof(types), CreateConstructorExceptionMessage(type.Assembly, type.FullName, types, flags)); } return constructor; } public static Type GetNestedTypeOrThrow(this Type type, string name, BindingFlags flags) { var result = type.GetNestedType(name, flags); if (result == null) { throw new ArgumentOutOfRangeException(nameof(name), CreateFieldExceptionMessage(type.Assembly, type.FullName, name, flags)); } return result; } public static Func CreateGetter(this Type type, string memberName, BindingFlags flags = DefaultBindingFlags) { return CreateGetter(type, memberName, flags); } public static Func CreateGetter(this Type type, string memberName, BindingFlags flags = DefaultBindingFlags) { var candidates = type.GetMembers(flags).Where(x => x.Name == memberName) .ToList(); if (candidates.Count > 1) { throw new InvalidOperationException($"Multiple members with name {memberName} found in type {type.FullName}"); } if (candidates.Count == 0) { throw new ArgumentOutOfRangeException(nameof(memberName),$"No members with name {memberName} found in type {type.FullName}"); } var candidate = candidates[0]; bool isStatic = false; switch (candidate) { case FieldInfo field: isStatic = field.IsStatic; break; case PropertyInfo property: isStatic = property.GetMethod.IsStatic; break; case MethodInfo method: isStatic = method.IsStatic; break; } if (isStatic) { var getter = CreateStaticAccessorInternal(candidate).GetValue; return _ => getter(); } else { return CreateAccessorInternal(candidate).GetValue; } } public static InstanceAccessor CreateFieldAccessor(this Type type, string fieldName, Type expectedFieldType = null, BindingFlags flags = DefaultBindingFlags) { return CreateFieldAccessor(type, fieldName, expectedFieldType); } public static InstanceAccessor CreateFieldAccessor(this Type type, string fieldName, Type expectedFieldType = null, BindingFlags flags = DefaultBindingFlags) { var field = type.GetFieldOrThrow(fieldName, expectedFieldType ?? typeof(FieldType), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return CreateAccessorInternal(field); } public static StaticAccessor CreateStaticFieldAccessor(this Type type, string fieldName, Type expectedFieldType = null) { return CreateStaticFieldAccessor(type, fieldName, expectedFieldType); } public static StaticAccessor CreateStaticFieldAccessor(this Type type, string fieldName, Type expectedFieldType = null) { var field = type.GetFieldOrThrow(fieldName, expectedFieldType ?? typeof(FieldType), BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); return CreateStaticAccessorInternal(field); } public static InstanceAccessor CreatePropertyAccessor(this Type type, string fieldName, Type expectedPropertyType = null, BindingFlags flags = DefaultBindingFlags) { var field = type.GetPropertyOrThrow(fieldName, expectedPropertyType ?? typeof(PropertyType), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return CreateAccessorInternal(field); } public static StaticAccessor CreateStaticPropertyAccessor(this Type type, string fieldName, Type expectedFieldType = null) { return CreateStaticPropertyAccessor(type, fieldName, expectedFieldType); } public static StaticAccessor CreateStaticPropertyAccessor(this Type type, string fieldName, Type expectedFieldType = null) { var field = type.GetPropertyOrThrow(fieldName, expectedFieldType ?? typeof(FieldType), BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); return CreateStaticAccessorInternal(field); } private static string CreateMethodExceptionMessage(Assembly assembly, string typeName, string methodName, BindingFlags flags) { return CreateMethodExceptionMessage(assembly, typeName, methodName, flags, typeof(T)); } private static string CreateMethodExceptionMessage(Assembly assembly, string typeName, string methodName, BindingFlags flags, Type delegateType) { return $"{assembly.FullName}.{typeName}.{methodName} with flags: {flags} and type: {delegateType}"; } private static string CreateFieldExceptionMessage(Assembly assembly, string typeName, string fieldName, BindingFlags flags) { return $"{assembly.FullName}.{typeName}.{fieldName} with flags: {flags}"; } private static string CreateConstructorExceptionMessage(Assembly assembly, string typeName, BindingFlags flags) { return $"{assembly.FullName}.{typeName}() with flags: {flags}"; } private static string CreateConstructorExceptionMessage(Assembly assembly, string typeName, Type[] types, BindingFlags flags) { return $"{assembly.FullName}.{typeName}({string.Join(", ", types.Select(x => x.FullName))}) with flags: {flags}"; } private static T CreateMethodDelegateInternal(this Type type, string name, BindingFlags flags) where T : Delegate { return (T)CreateMethodDelegateInternal(type, name, flags, typeof(T)); } private static Delegate CreateMethodDelegateInternal(this Type type, string name, BindingFlags flags, Type delegateType) { var method = GetMethodOrThrow(type, name, flags, delegateType); return Delegate.CreateDelegate(delegateType, null, method); } private static MethodInfo GetMethodOrThrow(Type type, string name, BindingFlags flags, Type delegateType) { return GetMethodOrThrow(type, name, flags, delegateType, Array.Empty(), out _); } private static MethodInfo FindMethod(Type type, string name, BindingFlags flags, Type returnType, params Type[] parameters) { var method = type.GetMethod(name, flags, null, parameters, null); if (method == null) { return null; } if (method.ReturnType != returnType) { return null; } return method; } private static ConstructorInfo GetConstructorOrThrow(Type type, BindingFlags flags, Type delegateType, DelegateSwizzle[] swizzles, out DelegateSwizzle firstMatchingSwizzle) { var delegateMethod = delegateType.GetMethod("Invoke"); var allDelegateParameters = delegateMethod.GetParameters().Select(x => x.ParameterType).ToArray(); var constructor = type.GetConstructor(flags, null, allDelegateParameters, null); if (constructor != null) { firstMatchingSwizzle = null; return constructor; } if (swizzles != null) { foreach (var swizzle in swizzles) { var swizzled = swizzle.Swizzle(allDelegateParameters); constructor = type.GetConstructor(flags, null, swizzled, null); if (constructor != null) { firstMatchingSwizzle = swizzle; return constructor; } } } var constructors = type.GetConstructors(flags); throw new ArgumentOutOfRangeException(nameof(delegateType), $"No matching constructor found for {type}, " + $"signature \"{delegateType}\", " + $"flags \"{flags}\" and " + $"params: {string.Join(", ", allDelegateParameters.Select(x => x.FullName))}" + $", candidates are\n: {string.Join("\n", constructors.Select(x => x.ToString()))}"); } private static MethodInfo GetMethodOrThrow(Type type, string name, BindingFlags flags, Type delegateType, DelegateSwizzle[] swizzles, out DelegateSwizzle firstMatchingSwizzle) { var delegateMethod = delegateType.GetMethod("Invoke"); var allDelegateParameters = delegateMethod.GetParameters().Select(x => x.ParameterType).ToArray(); var method = FindMethod(type, name, flags, delegateMethod.ReturnType, flags.HasFlag(BindingFlags.Static) ? allDelegateParameters : allDelegateParameters.Skip(1).ToArray()); if (method != null) { firstMatchingSwizzle = null; return method; } if (swizzles != null) { foreach (var swizzle in swizzles) { var swizzled = swizzle.Swizzle(allDelegateParameters); if (!flags.HasFlag(BindingFlags.Static) && swizzled[0] != type) { throw new InvalidOperationException(); } method = FindMethod(type, name, flags, delegateMethod.ReturnType, flags.HasFlag(BindingFlags.Static) ? swizzled : swizzled.Skip(1).ToArray()); if (method != null) { firstMatchingSwizzle = swizzle; return method; } } } var methods = type.GetMethods(flags); throw new ArgumentOutOfRangeException(nameof(name), $"No method found matching name \"{name}\", " + $"signature \"{delegateType}\", " + $"flags \"{flags}\" and " + $"params: {string.Join(", ", allDelegateParameters.Select(x => x.FullName))}" + $", candidates are\n: {string.Join("\n", methods.Select(x => x.ToString()))}"); } public static bool IsArrayOrList(this Type listType) { if (listType.IsArray) { return true; } if (listType.IsGenericType && listType.GetGenericTypeDefinition() == typeof(List<>)) { return true; } return false; } public static Type GetArrayOrListElementType(this Type listType) { if (listType.IsArray) { return listType.GetElementType(); } if (listType.IsGenericType && listType.GetGenericTypeDefinition() == typeof(List<>)) { return listType.GetGenericArguments()[0]; } return null; } public static Type MakeFuncType(params Type[] types) { return GetFuncType(types.Length).MakeGenericType(types); } private static Type GetFuncType(int argumentCount) { switch (argumentCount) { case 1: return typeof(Func<>); case 2: return typeof(Func<,>); case 3: return typeof(Func<,,>); case 4: return typeof(Func<,,,>); case 5: return typeof(Func<,,,,>); case 6: return typeof(Func<,,,,,>); default: throw new ArgumentOutOfRangeException(nameof(argumentCount)); } } public static Type MakeActionType(params Type[] types) { if (types.Length == 0) { return typeof(Action); } return GetActionType(types.Length).MakeGenericType(types); } private static Type GetActionType(int argumentCount) { switch (argumentCount) { case 1: return typeof(Action<>); case 2: return typeof(Action<,>); case 3: return typeof(Action<,,>); case 4: return typeof(Action<,,,>); case 5: return typeof(Action<,,,,>); case 6: return typeof(Action<,,,,,>); default: throw new ArgumentOutOfRangeException(nameof(argumentCount)); } } private static StaticAccessor CreateStaticAccessorInternal(MemberInfo member) { try { var valueParameter = Expression.Parameter(typeof(T), "value"); var canWrite = true; UnaryExpression valueExpression; Expression memberExpression; switch (member) { case PropertyInfo property: valueExpression = Expression.Convert(valueParameter, property.PropertyType); memberExpression = Expression.Property(null, property); canWrite = property.CanWrite; break; case FieldInfo field: valueExpression = Expression.Convert(valueParameter, field.FieldType); memberExpression = Expression.Field(null, field); canWrite = field.IsInitOnly == false; break; case MethodInfo method when method.GetParameters().Length == 0: valueExpression = null; memberExpression = Expression.Call(method); canWrite = false; break; default: throw new InvalidOperationException($"Unsupported member type {member.GetType().Name}"); } Func getter; var getExpression = Expression.Convert(memberExpression, typeof(T)); var getLambda = Expression.Lambda>(getExpression); getter = getLambda.Compile(); Action setter = null; if (canWrite) { var setExpression = Expression.Assign(memberExpression, valueExpression); var setLambda = Expression.Lambda>(setExpression, valueParameter); setter = setLambda.Compile(); } return new StaticAccessor { GetValue = getter, SetValue = setter }; } catch (Exception ex) { throw new InvalidOperationException($"Failed to create accessor for {member.DeclaringType}.{member.Name}", ex); } } private static InstanceAccessor CreateAccessorInternal(MemberInfo member) { try { var instanceParameter = Expression.Parameter(typeof(object), "instance"); var instanceExpression = Expression.Convert(instanceParameter, member.DeclaringType); var valueParameter = Expression.Parameter(typeof(T), "value"); var canWrite = true; UnaryExpression valueExpression; Expression memberExpression; switch (member) { case PropertyInfo property: valueExpression = Expression.Convert(valueParameter, property.PropertyType); memberExpression = Expression.Property(instanceExpression, property); canWrite = property.CanWrite; break; case FieldInfo field: valueExpression = Expression.Convert(valueParameter, field.FieldType); memberExpression = Expression.Field(instanceExpression, field); canWrite = field.IsInitOnly == false; break; case MethodInfo method when method.GetParameters().Length == 0: valueExpression = null; memberExpression = Expression.Call(instanceExpression, method); canWrite = false; break; default: throw new InvalidOperationException($"Unsupported member type {member.GetType().Name}"); } var getExpression = Expression.Convert(memberExpression, typeof(T)); var getLambda = Expression.Lambda>(getExpression, instanceParameter); var getter = getLambda.Compile(); Action setter = null; if (canWrite) { var setExpression = Expression.Assign(memberExpression, valueExpression); var setLambda = Expression.Lambda>(setExpression, instanceParameter, valueParameter); setter = setLambda.Compile(); } return new InstanceAccessor { GetValue = getter, SetValue = setter }; } catch (Exception ex) { throw new InvalidOperationException($"Failed to create accessor for {member.DeclaringType}.{member.Name}", ex); } } public struct InstanceAccessor { public Func GetValue; public Action SetValue; } public struct StaticAccessor { public Func GetValue; public Action SetValue; } public class DelegateSwizzle { private readonly int[] _args; public DelegateSwizzle(params int[] args) { _args = args; } public int Count => _args.Length; public T[] Swizzle(T[] inputTypes) { var result = new T[_args.Length]; for (var i = 0; i < _args.Length; ++i) { result[i] = inputTypes[_args[i]]; } return result; } } #if UNITY_EDITOR public static T CreateEditorMethodDelegate(string editorAssemblyTypeName, string methodName, BindingFlags flags = DefaultBindingFlags) where T : Delegate { return CreateMethodDelegate(typeof(Editor).Assembly, editorAssemblyTypeName, methodName, flags); } public static Delegate CreateEditorMethodDelegate(string editorAssemblyTypeName, string methodName, BindingFlags flags, Type delegateType) { return CreateMethodDelegate(typeof(Editor).Assembly, editorAssemblyTypeName, methodName, flags, delegateType); } #endif } } #endregion #region SerializedPropertyUtilities.cs namespace Fusion.Editor { using System; using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; using UnityEditor; static partial class SerializedPropertyUtilities { private static readonly Regex _arrayElementRegex = new(@"\.Array\.data\[\d+\]$", RegexOptions.Compiled); public static SerializedProperty FindPropertyOrThrow(this SerializedObject so, string propertyPath) { var result = so.FindProperty(propertyPath); if (result == null) { throw new ArgumentOutOfRangeException(nameof(propertyPath), $"Property not found: {propertyPath} on {so.targetObject}"); } return result; } public static SerializedProperty FindPropertyRelativeOrThrow(this SerializedProperty sp, string relativePropertyPath) { var result = sp.FindPropertyRelative(relativePropertyPath); if (result == null) { throw new ArgumentOutOfRangeException(nameof(relativePropertyPath), $"Property not found: {relativePropertyPath} (relative to \"{sp.propertyPath}\" of {sp.serializedObject.targetObject}"); } return result; } public static SerializedProperty FindPropertyRelativeToParentOrThrow(this SerializedProperty property, string relativePath) { var result = FindPropertyRelativeToParent(property, relativePath); if (result == null) { throw new ArgumentOutOfRangeException(nameof(relativePath), $"Property not found: {relativePath} (relative to the parent of \"{property.propertyPath}\" of {property.serializedObject.targetObject}"); } return result; } public static SerializedProperty FindPropertyRelativeToParent(this SerializedProperty property, string relativePath) { var parentPath = property.propertyPath; int startIndex = 0; do { // array element? if (parentPath.EndsWith("]")) { var match = _arrayElementRegex.Match(parentPath); if (match.Success) { parentPath = parentPath.Substring(0, match.Index); } } var lastDotIndex = parentPath.LastIndexOf('.'); if (lastDotIndex < 0) { if (string.IsNullOrEmpty(parentPath)) { return null; } parentPath = string.Empty; } else { parentPath = parentPath.Substring(0, lastDotIndex); } } while (relativePath[startIndex++] == '^'); if (startIndex > 1) { relativePath = relativePath.Substring(startIndex - 1); } if (string.IsNullOrEmpty(parentPath)) { return property.serializedObject.FindProperty(relativePath); } else { return property.serializedObject.FindProperty(parentPath + "." + relativePath); } } public static bool IsArrayElement(this SerializedProperty sp) { var propertyPath = sp.propertyPath; if (!propertyPath.EndsWith("]", StringComparison.Ordinal)) { return false; } return true; } public static bool IsArrayElement(this SerializedProperty sp, out int index) { var propertyPath = sp.propertyPath; if (!propertyPath.EndsWith("]", StringComparison.Ordinal)) { index = -1; return false; } var indexStart = propertyPath.LastIndexOf("[", StringComparison.Ordinal); if (indexStart < 0) { index = -1; return false; } index = int.Parse(propertyPath.Substring(indexStart + 1, propertyPath.Length - indexStart - 2)); return true; } public static SerializedProperty GetArrayFromArrayElement(this SerializedProperty sp) { var path = sp.propertyPath; var match = _arrayElementRegex.Match(path); if (!match.Success) { throw new ArgumentException($"Property is not an array element: {path}"); } var arrayPath = path.Substring(0, match.Index); return sp.serializedObject.FindProperty(arrayPath); } public static bool IsArrayProperty(this SerializedProperty sp) { return sp.isArray && sp.propertyType != SerializedPropertyType.String; } public static SerializedPropertyEnumerable GetChildren(this SerializedProperty property, bool visibleOnly = true) { return new SerializedPropertyEnumerable(property, visibleOnly); } public class SerializedPropertyEqualityComparer : IEqualityComparer { public static SerializedPropertyEqualityComparer Instance = new(); public bool Equals(SerializedProperty x, SerializedProperty y) { return SerializedProperty.DataEquals(x, y); } public int GetHashCode(SerializedProperty p) { bool enterChildren; var isFirst = true; var hashCode = 0; var minDepth = p.depth + 1; do { enterChildren = false; switch (p.propertyType) { case SerializedPropertyType.Integer: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.intValue); break; case SerializedPropertyType.Boolean: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.boolValue.GetHashCode()); break; case SerializedPropertyType.Float: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.floatValue.GetHashCode()); break; case SerializedPropertyType.String: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.stringValue.GetHashCode()); break; case SerializedPropertyType.Color: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.colorValue.GetHashCode()); break; case SerializedPropertyType.ObjectReference: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.objectReferenceInstanceIDValue); break; case SerializedPropertyType.LayerMask: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.intValue); break; case SerializedPropertyType.Enum: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.intValue); break; case SerializedPropertyType.Vector2: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.vector2Value.GetHashCode()); break; case SerializedPropertyType.Vector3: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.vector3Value.GetHashCode()); break; case SerializedPropertyType.Vector4: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.vector4Value.GetHashCode()); break; case SerializedPropertyType.Vector2Int: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.vector2IntValue.GetHashCode()); break; case SerializedPropertyType.Vector3Int: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.vector3IntValue.GetHashCode()); break; case SerializedPropertyType.Rect: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.rectValue.GetHashCode()); break; case SerializedPropertyType.RectInt: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.rectIntValue.GetHashCode()); break; case SerializedPropertyType.ArraySize: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.intValue); break; case SerializedPropertyType.Character: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.intValue.GetHashCode()); break; case SerializedPropertyType.AnimationCurve: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.animationCurveValue.GetHashCode()); break; case SerializedPropertyType.Bounds: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.boundsValue.GetHashCode()); break; case SerializedPropertyType.BoundsInt: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.boundsIntValue.GetHashCode()); break; case SerializedPropertyType.ExposedReference: hashCode = HashCodeUtilities.CombineHashCodes(hashCode, p.exposedReferenceValue.GetHashCode()); break; default: { enterChildren = true; break; } } if (isFirst) { if (!enterChildren) // no traverse needed { return hashCode; } // since property is going to be traversed, a copy needs to be made p = p.Copy(); isFirst = false; } } while (p.Next(enterChildren) && p.depth >= minDepth); return hashCode; } } public struct SerializedPropertyEnumerable : IEnumerable { private SerializedProperty property; private bool visible; public SerializedPropertyEnumerable(SerializedProperty property, bool visible) { this.property = property; this.visible = visible; } public SerializedPropertyEnumerator GetEnumerator() { return new SerializedPropertyEnumerator(property, visible); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public struct SerializedPropertyEnumerator : IEnumerator { private SerializedProperty current; private bool enterChildren; private bool visible; private int parentDepth; public SerializedPropertyEnumerator(SerializedProperty parent, bool visible) { current = parent.Copy(); enterChildren = true; parentDepth = parent.depth; this.visible = visible; } public SerializedProperty Current => current; SerializedProperty IEnumerator.Current => current; object IEnumerator.Current => current; public void Dispose() { current.Dispose(); } public bool MoveNext() { bool entered = visible ? current.NextVisible(enterChildren) : current.Next(enterChildren); enterChildren = false; if (!entered) { return false; } if (current.depth <= parentDepth) { return false; } return true; } public void Reset() { throw new NotImplementedException(); } } private static int[] _updateFixedBufferTemp = Array.Empty(); internal static bool UpdateFixedBuffer(this SerializedProperty sp, Action fill, Action update, bool write, bool force = false) { int count = sp.fixedBufferSize; Array.Resize(ref _updateFixedBufferTemp, Math.Max(_updateFixedBufferTemp.Length, count)); // need to get to the first property... `GetFixedBufferElementAtIndex` is slow and allocates var element = sp.Copy(); element.Next(true); // .Array element.Next(true); // .Array.size element.Next(true); // .Array.data[0] unsafe { fixed (int* p = _updateFixedBufferTemp) { Unity.Collections.LowLevel.Unsafe.UnsafeUtility.MemClear(p, count * sizeof(int)); } fill(_updateFixedBufferTemp, count); int i = 0; if (!force) { // find the first difference for (; i < count; ++i, element.Next(true)) { FusionEditorLog.Assert(element.propertyType == SerializedPropertyType.Integer, "Invalid property type, expected integer"); if (element.intValue != _updateFixedBufferTemp[i]) { break; } } } if (i < count) { // update data if (write) { for (; i < count; ++i, element.Next(true)) { element.intValue = _updateFixedBufferTemp[i]; } } else { for (; i < count; ++i, element.Next(true)) { _updateFixedBufferTemp[i] = element.intValue; } } update(_updateFixedBufferTemp, count); return true; } else { return false; } } } } } #endregion #region UnityInternal.cs namespace Fusion.Editor { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; using static ReflectionUtils; static partial class UnityInternal { static Assembly FindAssembly(string name) { return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == name); } [UnityEditor.InitializeOnLoad] public static class Event { static StaticAccessor s_Current_ = typeof(UnityEngine.Event).CreateStaticFieldAccessor(nameof(s_Current)); public static UnityEngine.Event s_Current => s_Current_.GetValue(); } [UnityEditor.InitializeOnLoad] public static class Editor { public delegate bool DoDrawDefaultInspectorDelegate(SerializedObject obj); public delegate void BoolSetterDelegate(UnityEditor.Editor editor, bool value); public static readonly DoDrawDefaultInspectorDelegate DoDrawDefaultInspector = typeof(UnityEditor.Editor).CreateMethodDelegate(nameof(DoDrawDefaultInspector)); public static readonly BoolSetterDelegate InternalSetHidden = typeof(UnityEditor.Editor).CreateMethodDelegate(nameof(InternalSetHidden), BindingFlags.NonPublic | BindingFlags.Instance); } [UnityEditor.InitializeOnLoad] public static class EditorGUI { public delegate string DelayedTextFieldInternalDelegate(Rect position, int id, GUIContent label, string value, string allowedLetters, GUIStyle style); public delegate Rect MultiFieldPrefixLabelDelegate(Rect totalPosition, int id, GUIContent label, int columns); public delegate string TextFieldInternalDelegate(int id, Rect position, string text, GUIStyle style); public delegate string ToolbarSearchFieldDelegate(int id, Rect position, string text, bool showWithPopupArrow); public delegate bool DefaultPropertyFieldDelegate(Rect position, UnityEditor.SerializedProperty property, GUIContent label); public static readonly MultiFieldPrefixLabelDelegate MultiFieldPrefixLabel = typeof(UnityEditor.EditorGUI).CreateMethodDelegate(nameof(MultiFieldPrefixLabel)); public static readonly TextFieldInternalDelegate TextFieldInternal = typeof(UnityEditor.EditorGUI).CreateMethodDelegate(nameof(TextFieldInternal)); public static readonly ToolbarSearchFieldDelegate ToolbarSearchField = typeof(UnityEditor.EditorGUI).CreateMethodDelegate(nameof(ToolbarSearchField)); public static readonly DelayedTextFieldInternalDelegate DelayedTextFieldInternal = typeof(UnityEditor.EditorGUI).CreateMethodDelegate(nameof(DelayedTextFieldInternal)); public static readonly DefaultPropertyFieldDelegate DefaultPropertyField = typeof(UnityEditor.EditorGUI).CreateMethodDelegate(nameof(DefaultPropertyField)); private static readonly FieldInfo s_TextFieldHash = typeof(UnityEditor.EditorGUI).GetFieldOrThrow(nameof(s_TextFieldHash)); private static readonly FieldInfo s_DelayedTextFieldHash = typeof(UnityEditor.EditorGUI).GetFieldOrThrow(nameof(s_DelayedTextFieldHash)); private static readonly StaticAccessor s_indent = typeof(UnityEditor.EditorGUI).CreateStaticPropertyAccessor(nameof(indent)); public static readonly Action EndEditingActiveTextField = typeof(UnityEditor.EditorGUI).CreateMethodDelegate(nameof(EndEditingActiveTextField)); public static int TextFieldHash => (int)s_TextFieldHash.GetValue(null); public static int DelayedTextFieldHash => (int)s_DelayedTextFieldHash.GetValue(null); internal static float indent => s_indent.GetValue(); } [UnityEditor.InitializeOnLoad] public static class EditorUtility { public delegate void DisplayCustomMenuDelegate(Rect position, string[] options, int[] selected, UnityEditor.EditorUtility.SelectMenuItemFunction callback, object userData); public static DisplayCustomMenuDelegate DisplayCustomMenu = typeof(UnityEditor.EditorUtility).CreateMethodDelegate(nameof(DisplayCustomMenu), BindingFlags.NonPublic | BindingFlags.Static); } [UnityEditor.InitializeOnLoad] public static class HandleUtility { public static readonly Action ApplyWireMaterial = typeof(UnityEditor.HandleUtility).CreateMethodDelegate(nameof(ApplyWireMaterial)); } [UnityEditor.InitializeOnLoad] public static class LayerMatrixGUI { private const string TypeName = #if UNITY_2023_1_OR_NEWER "UnityEditor.LayerCollisionMatrixGUI2D"; #else "UnityEditor.LayerMatrixGUI"; #endif private static readonly Type InternalType = #if UNITY_2023_1_OR_NEWER FindAssembly("UnityEditor.Physics2DModule")?.GetType(TypeName, true); #else typeof(UnityEditor.Editor).Assembly.GetType(TypeName, true); #endif private static readonly Type InternalGetValueFuncType = InternalType?.GetNestedTypeOrThrow(nameof(GetValueFunc), BindingFlags.Public); private static readonly Type InternalSetValueFuncType = InternalType?.GetNestedTypeOrThrow(nameof(SetValueFunc), BindingFlags.Public); #if UNITY_2023_1_OR_NEWER private static readonly Delegate _Draw = InternalType?.CreateMethodDelegate(nameof(Draw), BindingFlags.Public | BindingFlags.Static, typeof(Action<,,>).MakeGenericType( typeof(GUIContent), InternalGetValueFuncType, InternalSetValueFuncType) ); #else private delegate void Ref2Action(T1 t1, ref T2 t2, T3 t3, T4 t4); private static readonly Delegate _DoGUI = InternalType?.CreateMethodDelegate("DoGUI", BindingFlags.Public | BindingFlags.Static, typeof(Ref2Action<,,,>).MakeGenericType( typeof(GUIContent), typeof(bool), InternalGetValueFuncType, InternalSetValueFuncType) ); #endif public delegate bool GetValueFunc(int layerA, int layerB); public delegate void SetValueFunc(int layerA, int layerB, bool val); public static void Draw(GUIContent label, GetValueFunc getValue, SetValueFunc setValue) { if (InternalType == null) { throw new InvalidOperationException($"{TypeName} not found"); } var getter = Delegate.CreateDelegate(InternalGetValueFuncType, getValue.Target, getValue.Method); var setter = Delegate.CreateDelegate(InternalSetValueFuncType, setValue.Target, setValue.Method); #if UNITY_2023_1_OR_NEWER _Draw.DynamicInvoke(label, getter, setter); #else bool show = true; var args = new object[] { label, show, getter, setter }; _DoGUI.DynamicInvoke(args); #endif } } [UnityEditor.InitializeOnLoad] public static class DecoratorDrawer { private static InstanceAccessor m_Attribute = typeof(UnityEditor.DecoratorDrawer).CreateFieldAccessor(nameof(m_Attribute)); public static void SetAttribute(UnityEditor.DecoratorDrawer drawer, PropertyAttribute attribute) { m_Attribute.SetValue(drawer, attribute); } } [UnityEditor.InitializeOnLoad] public static class PropertyDrawer { private static InstanceAccessor m_Attribute = typeof(UnityEditor.PropertyDrawer).CreateFieldAccessor(nameof(m_Attribute)); private static InstanceAccessor m_FieldInfo = typeof(UnityEditor.PropertyDrawer).CreateFieldAccessor(nameof(m_FieldInfo)); public static void SetAttribute(UnityEditor.PropertyDrawer drawer, PropertyAttribute attribute) { m_Attribute.SetValue(drawer, attribute); } public static void SetFieldInfo(UnityEditor.PropertyDrawer drawer, FieldInfo fieldInfo) { m_FieldInfo.SetValue(drawer, fieldInfo); } } [UnityEditor.InitializeOnLoad] public static class EditorGUIUtility { private static readonly StaticAccessor s_LastControlID = typeof(UnityEditor.EditorGUIUtility).CreateStaticFieldAccessor(nameof(s_LastControlID)); private static readonly StaticAccessor _contentWidth = typeof(UnityEditor.EditorGUIUtility).CreateStaticPropertyAccessor(nameof(contextWidth)); public static int LastControlID => s_LastControlID.GetValue(); public static float contextWidth => _contentWidth.GetValue(); public delegate UnityEngine.Object GetScriptDelegate(string scriptClass); public delegate Texture2D GetIconForObjectDelegate(UnityEngine.Object obj); public delegate GUIContent TempContentDelegate(string text); public delegate Texture2D GetHelpIconDelegate(MessageType type); public static readonly GetScriptDelegate GetScript = typeof(UnityEditor.EditorGUIUtility).CreateMethodDelegate(nameof(GetScript)); public static readonly GetIconForObjectDelegate GetIconForObject = typeof(UnityEditor.EditorGUIUtility).CreateMethodDelegate(nameof(GetIconForObject)); public static readonly TempContentDelegate TempContent = typeof(UnityEditor.EditorGUIUtility).CreateMethodDelegate(nameof(TempContent)); public static readonly GetHelpIconDelegate GetHelpIcon = typeof(UnityEditor.EditorGUIUtility).CreateMethodDelegate(nameof(GetHelpIcon)); } [UnityEditor.InitializeOnLoad] public static class ScriptAttributeUtility { public delegate List GetFieldAttributesDelegate(FieldInfo field); public delegate FieldInfo GetFieldInfoFromPropertyDelegate(SerializedProperty property, out Type type); public static readonly Type InternalType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.ScriptAttributeUtility", true); public static readonly GetFieldInfoFromPropertyDelegate GetFieldInfoFromProperty = CreateEditorMethodDelegate( "UnityEditor.ScriptAttributeUtility", "GetFieldInfoFromProperty", BindingFlags.Static | BindingFlags.NonPublic); public delegate Type GetDrawerTypeForPropertyAndTypeDelegate(SerializedProperty property, Type type); public delegate Type GetDrawerTypeForTypeDelegate(Type type, Type[] renderPipelineTypes, bool isManagedReference); #if UNITY_2023_3_OR_NEWER public static readonly GetDrawerTypeForPropertyAndTypeDelegate GetDrawerTypeForPropertyAndType = CreateEditorMethodDelegate( "UnityEditor.ScriptAttributeUtility", nameof(GetDrawerTypeForPropertyAndType), BindingFlags.Static | BindingFlags.NonPublic); public static readonly GetDrawerTypeForTypeDelegate GetDrawerTypeForType = CreateEditorMethodDelegate( "UnityEditor.ScriptAttributeUtility", nameof(GetDrawerTypeForType), BindingFlags.Static | BindingFlags.NonPublic); #else private delegate Type LegacyGetDrawerTypeForTypeDelegate(Type type); private static readonly LegacyGetDrawerTypeForTypeDelegate LegacyGetDrawerTypeForType = CreateEditorMethodDelegate( "UnityEditor.ScriptAttributeUtility", "GetDrawerTypeForType", BindingFlags.Static | BindingFlags.NonPublic); public static readonly GetDrawerTypeForPropertyAndTypeDelegate GetDrawerTypeForPropertyAndType = (_, type) => LegacyGetDrawerTypeForType(type); public static readonly GetDrawerTypeForTypeDelegate GetDrawerTypeForType = (type, _, _) => LegacyGetDrawerTypeForType(type); #endif private static readonly GetHandlerDelegate _GetHandler = InternalType.CreateMethodDelegate("GetHandler", BindingFlags.NonPublic | BindingFlags.Static, MakeFuncType(typeof(SerializedProperty), PropertyHandler.InternalType) ); public static readonly GetFieldAttributesDelegate GetFieldAttributes = InternalType.CreateMethodDelegate(nameof(GetFieldAttributes)); private static readonly StaticAccessor _propertyHandlerCache = InternalType.CreateStaticPropertyAccessor(nameof(propertyHandlerCache), PropertyHandlerCache.InternalType); private static readonly StaticAccessor s_SharedNullHandler = InternalType.CreateStaticFieldAccessor("s_SharedNullHandler", PropertyHandler.InternalType); private static readonly StaticAccessor s_NextHandler = InternalType.CreateStaticFieldAccessor("s_NextHandler", PropertyHandler.InternalType); public static PropertyHandlerCache propertyHandlerCache => new() { _instance = _propertyHandlerCache.GetValue() }; public static PropertyHandler sharedNullHandler => PropertyHandler.Wrap(s_SharedNullHandler.GetValue()); public static PropertyHandler nextHandler => PropertyHandler.Wrap(s_NextHandler.GetValue()); public static PropertyHandler GetHandler(SerializedProperty property) { return PropertyHandler.Wrap(_GetHandler(property)); } private delegate object GetHandlerDelegate(SerializedProperty property); } public struct PropertyHandlerCache { [UnityEditor.InitializeOnLoad] private static class Statics { public static readonly Type InternalType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.PropertyHandlerCache", true); public static readonly GetPropertyHashDelegate GetPropertyHash = InternalType.CreateMethodDelegate(nameof(GetPropertyHash)); public static readonly GetHandlerDelegate GetHandler = InternalType.CreateMethodDelegate(nameof(GetHandler), BindingFlags.NonPublic | BindingFlags.Instance, MakeFuncType(InternalType, typeof(SerializedProperty), PropertyHandler.InternalType)); public static readonly SetHandlerDelegate SetHandler = InternalType.CreateMethodDelegate(nameof(SetHandler), BindingFlags.NonPublic | BindingFlags.Instance, MakeActionType(InternalType, typeof(SerializedProperty), PropertyHandler.InternalType)); public static readonly FieldInfo m_PropertyHandlers = InternalType.GetFieldOrThrow(nameof(m_PropertyHandlers)); } public static Type InternalType => Statics.InternalType; public delegate int GetPropertyHashDelegate(SerializedProperty property); public delegate object GetHandlerDelegate(object instance, SerializedProperty property); public delegate void SetHandlerDelegate(object instance, SerializedProperty property, object handlerInstance); public object _instance; public PropertyHandler GetHandler(SerializedProperty property) { return new PropertyHandler { _instance = Statics.GetHandler(_instance, property) }; } public void SetHandler(SerializedProperty property, PropertyHandler newHandler) { Statics.SetHandler(_instance, property, newHandler._instance); } public IEnumerable<(int, PropertyHandler)> PropertyHandlers { get { var dict = (IDictionary)Statics.m_PropertyHandlers.GetValue(_instance); foreach (DictionaryEntry entry in dict) { yield return ((int)entry.Key, PropertyHandler.Wrap(entry.Value)); } } } } public struct PropertyHandler : IEquatable { [UnityEditor.InitializeOnLoad] private static class Statics { public static readonly Type InternalType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.PropertyHandler", true); public static readonly InstanceAccessor> m_DecoratorDrawers = InternalType.CreateFieldAccessor>(nameof(m_DecoratorDrawers)); public static readonly InstanceAccessor> m_PropertyDrawers = InternalType.CreateFieldAccessor>(nameof(m_PropertyDrawers)); } public static Type InternalType => Statics.InternalType; public object _instance; internal static PropertyHandler Wrap(object instance) { return new() { _instance = instance }; } public static PropertyHandler New() { return Wrap(Activator.CreateInstance(InternalType)); } public List m_PropertyDrawers { get => Statics.m_PropertyDrawers.GetValue(_instance); set => Statics.m_PropertyDrawers.SetValue(_instance, value); } public bool Equals(PropertyHandler other) { return _instance == other._instance; } public override int GetHashCode() { return _instance?.GetHashCode() ?? 0; } public override bool Equals(object obj) { return obj is PropertyHandler h ? Equals(h) : false; } public List decoratorDrawers { get => Statics.m_DecoratorDrawers.GetValue(_instance); set => Statics.m_DecoratorDrawers.SetValue(_instance, value); } } [UnityEditor.InitializeOnLoad] public static class EditorApplication { public static readonly Action Internal_CallAssetLabelsHaveChanged = typeof(UnityEditor.EditorApplication).CreateMethodDelegate(nameof(Internal_CallAssetLabelsHaveChanged)); } public struct ObjectSelector { [UnityEditor.InitializeOnLoad] private static class Statics { public static readonly Type InternalType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.ObjectSelector", true); public static readonly StaticAccessor _tooltip = InternalType.CreateStaticPropertyAccessor(nameof(isVisible)); public static readonly StaticAccessor _get = InternalType.CreateStaticPropertyAccessor(nameof(get), InternalType); public static readonly InstanceAccessor _searchFilter = InternalType.CreatePropertyAccessor(nameof(searchFilter)); } private EditorWindow _instance; public static bool isVisible => Statics._tooltip.GetValue(); public static ObjectSelector get => new() { _instance = Statics._get.GetValue() }; public string searchFilter { get => Statics._searchFilter.GetValue(_instance); set => Statics._searchFilter.SetValue(_instance, value); } private static readonly InstanceAccessor _objectSelectorID = Statics.InternalType.CreateFieldAccessor(nameof(objectSelectorID)); public int objectSelectorID => _objectSelectorID.GetValue(_instance); } [UnityEditor.InitializeOnLoad] public class InspectorWindow { public static readonly Type InternalType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.InspectorWindow", true); public static readonly InstanceAccessor _isLockedAccessor = InternalType.CreatePropertyAccessor(nameof(isLocked)); private readonly EditorWindow _instance; public InspectorWindow(EditorWindow instance) { if (instance == null) { throw new ArgumentNullException(nameof(instance)); } _instance = instance; } public bool isLocked { get => _isLockedAccessor.GetValue(_instance); set => _isLockedAccessor.SetValue(_instance, value); } } [UnityEditor.InitializeOnLoad] public static class SplitterGUILayout { public static readonly Action EndHorizontalSplit = CreateMethodDelegate(typeof(UnityEditor.Editor).Assembly, "UnityEditor.SplitterGUILayout", "EndHorizontalSplit", BindingFlags.Public | BindingFlags.Static ); public static readonly Action EndVerticalSplit = CreateMethodDelegate(typeof(UnityEditor.Editor).Assembly, "UnityEditor.SplitterGUILayout", "EndVerticalSplit", BindingFlags.Public | BindingFlags.Static ); public static void BeginHorizontalSplit(SplitterState splitterState, GUIStyle style, params GUILayoutOption[] options) { _beginHorizontalSplit.DynamicInvoke(splitterState.InternalState, style, options); } public static void BeginVerticalSplit(SplitterState splitterState, GUIStyle style, params GUILayoutOption[] options) { _beginVerticalSplit.DynamicInvoke(splitterState.InternalState, style, options); } private static readonly Delegate _beginHorizontalSplit = CreateMethodDelegate(typeof(UnityEditor.Editor).Assembly, "UnityEditor.SplitterGUILayout", "BeginHorizontalSplit", BindingFlags.Public | BindingFlags.Static, typeof(Action<,,>).MakeGenericType(SplitterState.InternalType, typeof(GUIStyle), typeof(GUILayoutOption[])) ); private static readonly Delegate _beginVerticalSplit = CreateMethodDelegate(typeof(UnityEditor.Editor).Assembly, "UnityEditor.SplitterGUILayout", "BeginVerticalSplit", BindingFlags.Public | BindingFlags.Static, typeof(Action<,,>).MakeGenericType(SplitterState.InternalType, typeof(GUIStyle), typeof(GUILayoutOption[])) ); } [UnityEditor.InitializeOnLoad] [Serializable] public class SplitterState : ISerializationCallbackReceiver { public static readonly Type InternalType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.SplitterState", true); private static readonly FieldInfo _relativeSizes = InternalType.GetFieldOrThrow("relativeSizes"); private static readonly FieldInfo _realSizes = InternalType.GetFieldOrThrow("realSizes"); private static readonly FieldInfo _splitSize = InternalType.GetFieldOrThrow("splitSize"); public string Json = "{}"; [NonSerialized] public object InternalState = FromRelativeInner(new[] { 1.0f }); void ISerializationCallbackReceiver.OnAfterDeserialize() { InternalState = JsonUtility.FromJson(Json, InternalType); } void ISerializationCallbackReceiver.OnBeforeSerialize() { Json = JsonUtility.ToJson(InternalState); } public static SplitterState FromRelative(float[] relativeSizes, int[] minSizes = null, int[] maxSizes = null, int splitSize = 0) { var result = new SplitterState(); result.InternalState = FromRelativeInner(relativeSizes, minSizes, maxSizes, splitSize); return result; } private static object FromRelativeInner(float[] relativeSizes, int[] minSizes = null, int[] maxSizes = null, int splitSize = 0) { return Activator.CreateInstance(InternalType, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, new object[] { relativeSizes, minSizes, maxSizes, splitSize }, null, null); } public float[] realSizes => ConvertArray((Array)_realSizes.GetValue(InternalState)); public float[] relativeSizes => ConvertArray((Array)_relativeSizes.GetValue(InternalState)); public float splitSize => Convert.ToSingle(_splitSize.GetValue(InternalState)); private static float[] ConvertArray(Array value) { float[] result = new float[value.Length]; for (int i = 0; i < value.Length; ++i) { result[i] = Convert.ToSingle(value.GetValue(i)); } return result; } } public sealed class InternalStyles { public static InternalStyles Instance = new InternalStyles(); public LazyGUIStyle InspectorTitlebar => LazyGUIStyle.Create(_ => GetStyle("IN Title")); public LazyGUIStyle FoldoutTitlebar => LazyGUIStyle.Create(_ => GetStyle("Titlebar Foldout", "Foldout")); public LazyGUIStyle BoxWithBorders => LazyGUIStyle.Create(_ => GetStyle("OL Box")); public LazyGUIStyle HierarchyTreeViewLine => LazyGUIStyle.Create(_ => GetStyle("TV Line")); public LazyGUIStyle HierarchyTreeViewSceneBackground => LazyGUIStyle.Create(_ => GetStyle("SceneTopBarBg", "ProjectBrowserTopBarBg")); public LazyGUIStyle OptionsButtonStyle => LazyGUIStyle.Create(_ => GetStyle("PaneOptions")); public LazyGUIStyle AddComponentButton => LazyGUIStyle.Create(_ => GetStyle("AC Button")); public LazyGUIStyle AnimationEventTooltip => LazyGUIStyle.Create(_ => GetStyle("AnimationEventTooltip")); public LazyGUIStyle AnimationEventTooltipArrow => LazyGUIStyle.Create(_ => GetStyle("AnimationEventTooltipArrow")); private static GUIStyle GetStyle(params string[] names) { var skin = GUI.skin; foreach (var name in names) { var result = skin.FindStyle(name); if (result != null) { return result; } } throw new ArgumentOutOfRangeException($"Style not found: {string.Join(", ", names)}", nameof(names)); } } public static InternalStyles Styles => InternalStyles.Instance; } } #endregion #region ArrayLengthAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; internal partial class ArrayLengthAttributeDrawer : DecoratingPropertyAttributeDrawer, INonApplicableOnArrayElements { private GUIStyle _style; private GUIStyle GetStyle() { if (_style == null) { _style = new GUIStyle(EditorStyles.miniLabel); _style.alignment = TextAnchor.MiddleRight; _style.contentOffset = new Vector2(-2, 0); _style.normal.textColor = EditorGUIUtility.isProSkin ? new Color(255f / 255f, 221 / 255f, 0 / 255f, 1f) : Color.blue; } return _style; } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { base.OnGUIInternal(position, property, label); if (!property.isArray) { return; } var overlayRect = position; overlayRect.height = EditorGUIUtility.singleLineHeight; var attrib = (ArrayLengthAttribute)attribute; // draw length overlay GUI.Label(overlayRect, $"[{attrib.MaxLength}]", GetStyle()); if (property.arraySize > attrib.MaxLength) { property.arraySize = attrib.MaxLength; property.serializedObject.ApplyModifiedProperties(); } else if (property.arraySize < attrib.MinLength) { property.arraySize = attrib.MinLength; property.serializedObject.ApplyModifiedProperties(); } } } [CustomPropertyDrawer(typeof(ArrayLengthAttribute))] [RedirectCustomPropertyDrawer(typeof(ArrayLengthAttribute), typeof(ArrayLengthAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region AssemblyNameAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(AssemblyNameAttribute))] internal class AssemblyNameAttributeDrawer : PropertyDrawerWithErrorHandling { const float DropdownWidth = 20.0f; static GUIContent DropdownContent = new GUIContent(""); string _lastCheckedAssemblyName; [Flags] enum AsmDefType { Predefined = 1 << 0, InPackages = 1 << 1, InAssets = 1 << 2, Editor = 1 << 3, Runtime = 1 << 4, All = Predefined | InPackages | InAssets | Editor | Runtime, } HashSet _allAssemblies; protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var assemblyName = property.stringValue; bool notFound = false; if (!string.IsNullOrEmpty(assemblyName)) { if (_allAssemblies == null) { _allAssemblies = new HashSet(GetAssemblies(AsmDefType.All), StringComparer.OrdinalIgnoreCase); } if (!_allAssemblies.Contains(assemblyName, StringComparer.OrdinalIgnoreCase)) { SetInfo($"Assembly not found: {assemblyName}"); notFound = true; } } using (new FusionEditorGUI.PropertyScope(position, label, property)) { EditorGUI.BeginChangeCheck(); assemblyName = EditorGUI.TextField(new Rect(position) { xMax = position.xMax - DropdownWidth }, label, assemblyName, notFound ? new GUIStyle(EditorStyles.textField) { fontStyle = FontStyle.Italic, normal = new GUIStyleState() { textColor = Color.gray } } : EditorStyles.textField ); var dropdownRect = EditorGUI.IndentedRect(new Rect(position) { xMin = position.xMax - DropdownWidth }); if (EditorGUI.DropdownButton(dropdownRect, DropdownContent, FocusType.Passive)) { GenericMenu.MenuFunction2 onClicked = (userData) => { property.stringValue = (string)userData; property.serializedObject.ApplyModifiedProperties(); UnityInternal.EditorGUI.EndEditingActiveTextField(); ClearError(property); }; var menu = new GenericMenu(); foreach (var (flag, prefix) in new[] { (AsmDefType.Editor, "Editor/"), (AsmDefType.Runtime, "") }) { if (menu.GetItemCount() != 0) { menu.AddSeparator(prefix); } foreach (var name in GetAssemblies(flag | AsmDefType.InPackages)) { menu.AddItem(new GUIContent($"{prefix}Packages/{name}"), string.Equals(name, assemblyName, StringComparison.OrdinalIgnoreCase), onClicked, name); } menu.AddSeparator(prefix); foreach (var name in GetAssemblies(flag | AsmDefType.InAssets | AsmDefType.Predefined)) { menu.AddItem(new GUIContent($"{prefix}{name}"), string.Equals(name, assemblyName, StringComparison.OrdinalIgnoreCase), onClicked, name); } } menu.DropDown(dropdownRect); } if (EditorGUI.EndChangeCheck()) { property.stringValue = assemblyName; property.serializedObject.ApplyModifiedProperties(); base.ClearError(); } } } static IEnumerable GetAssemblies(AsmDefType types) { IEnumerable query = Enumerable.Empty(); if (types.HasFlag(AsmDefType.Predefined)) { if (types.HasFlag(AsmDefType.Runtime)) { query = query.Concat(new[] { "Assembly-CSharp-firstpass", "Assembly-CSharp" }); } if (types.HasFlag(AsmDefType.Editor)) { query = query.Concat(new[] { "Assembly-CSharp-Editor-firstpass", "Assembly-CSharp-Editor" }); } } if (types.HasFlag(AsmDefType.InAssets) || types.HasFlag(AsmDefType.InPackages)) { query = query.Concat( AssetDatabase.FindAssets("t:asmdef") .Select(x => AssetDatabase.GUIDToAssetPath(x)) .Where(x => { if (types.HasFlag(AsmDefType.InAssets) && x.StartsWith("Assets/")) { return true; } else if (types.HasFlag(AsmDefType.InPackages) && x.StartsWith("Packages/")) { return true; } else { return false; } }) .Select(x => JsonUtility.FromJson(File.ReadAllText(x))) .Where(x => { bool editorOnly = x.includePlatforms.Length == 1 && x.includePlatforms[0] == "Editor"; if (types.HasFlag(AsmDefType.Runtime) && !editorOnly) { return true; } else if (types.HasFlag(AsmDefType.Editor) && editorOnly) { return true; } else { return false; } }) .Select(x => x.name) .Distinct() ); } return query; } [Serializable] private class AsmDefData { public string[] includePlatforms = Array.Empty(); public string name = string.Empty; } } } #endregion #region BinaryDataAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Text; using UnityEngine; using UnityEditor; internal partial class BinaryDataAttributeDrawer : PropertyDrawerWithErrorHandling, INonApplicableOnArrayElements { private int MaxLines = 16; private RawDataDrawer _drawer = new RawDataDrawer(); protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { using (new FusionEditorGUI.PropertyScope(position, label, property)) { bool wasExpanded = property.isExpanded; var foldoutPosition = new Rect(position) { height = EditorGUIUtility.singleLineHeight }; property.isExpanded = EditorGUI.Foldout(foldoutPosition, property.isExpanded, label); if (property.hasMultipleDifferentValues) { FusionEditorGUI.Overlay(foldoutPosition, $"---"); } else { FusionEditorGUI.Overlay(foldoutPosition, $"{property.arraySize}"); } if (!wasExpanded) { return; } position.yMin += foldoutPosition.height + EditorGUIUtility.standardVerticalSpacing; using (new FusionEditorGUI.EnabledScope(true)) { _drawer.Draw(GUIContent.none, position); } } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { if (!property.isExpanded) { return EditorGUIUtility.singleLineHeight; } _drawer.Refresh(property); // space for scrollbar and indent var width = UnityInternal.EditorGUIUtility.contextWidth - 32.0f; var height = _drawer.GetHeight(width); return EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing + Mathf.Min(FusionEditorGUI.GetLinesHeight(MaxLines), height); } } [CustomPropertyDrawer(typeof(BinaryDataAttribute))] [RedirectCustomPropertyDrawer(typeof(BinaryDataAttribute), typeof(BinaryDataAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region BitSetAttributeDrawer.cs // namespace Fusion.Editor { // using System; // using UnityEditor; // using UnityEngine; // // [CustomPropertyDrawer(typeof(BitSetAttribute))] // public class BitSetAttributeDrawer : PropertyDrawer { // // public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // // if (property.IsArrayElement()) { // throw new NotSupportedException(); // } // // var longValue = property.longValue; // // int bitStart = 0; // int bitEnd = ((BitSetAttribute)attribute).BitCount; // // using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out var valueRect)) { // var pos = valueRect; // // DrawAndMeasureLabel(valueRect, bitStart, FusionEditorSkin.instance.MiniLabelLowerRight); // DrawAndMeasureLabel(valueRect, bitEnd, FusionEditorSkin.instance.MiniLabelLowerLeft); // // var tmpContent = new GUIContent(); // tmpContent.text = $"{bitStart}"; // var bitStartSize = EditorStyles.miniLabel.CalcSize(tmpContent); // // // tmpContent.text = $"{bitEnd}"; // var bitEndSize = EditorStyles.miniLabel.CalcSize(tmpContent); // valueRect.width = bitEndSize.x; // GUI.Label(valueRect, tmpContent, EditorStyles.miniLabel); // valueRect.x += bitEndSize.x; // var availableWidth = valueRect.width - bitStartSize.x - bitEndSize.x; // // // // how may per one line? // const float ToggleWidth = 15.0f; // // valueRect.width = ToggleWidth; // for (int i = 0; i < 16; ++i) { // EditorGUI.Toggle(valueRect, false); // valueRect.x += ToggleWidth; // } // } // // float DrawAndMeasureLabel(Rect position, int label, GUIStyle style) { // var tmpContent = new GUIContent($"{bitEnd}"); // var contentSize = style.CalcSize(tmpContent); // GUI.Label(position, tmpContent, style); // return contentSize.x; // } // // //base.OnGUI(position, property, label); // } // } // } #endregion #region DecoratingPropertyAttributeDrawer.cs //#define FUSION_EDITOR_TRACE namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEditor; using UnityEngine; public abstract class DecoratingPropertyAttributeDrawer : PropertyDrawer { private bool _isLastDrawer; private int _nestingLevel; /// /// The drawer that's been chosen by Unity; its job is to /// iterate all ForwardingPropertyDrawerBase drawers /// that'd be created had Unity 2020.3 supported multiple /// property drawers - including self. /// protected DecoratingPropertyAttributeDrawer MainDrawer { get; private set; } public List PropertyDrawers { get; private set; } public PropertyDrawer NextDrawer { get; private set; } public DecoratingPropertyAttributeDrawer() { TraceField("constructor"); } [Obsolete("Derived classes should override and call OnGUIInternal", true)] #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member public sealed override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { #pragma warning restore CS0809 // Obsolete member overrides non-obsolete member TraceField($"OnGUI({position}, {property.propertyPath}, {label})"); EnsureInitialized(property); FusionEditorLog.Assert(MainDrawer == this); FusionEditorLog.Assert(PropertyDrawers != null); FusionEditorLog.Assert(PropertyDrawers.Count > 0); PropertyDrawers[0].InvokeOnGUIInternal(position, property, label); } [Obsolete("Derived classes should override and call GetPropertyHeightInternal", true)] #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member public sealed override float GetPropertyHeight(SerializedProperty property, GUIContent label) { #pragma warning restore CS0809 // Obsolete member overrides non-obsolete member TraceField($"GetPropertyHeight({property.propertyPath}, {label})"); EnsureInitialized(property); FusionEditorLog.Assert(MainDrawer == this); FusionEditorLog.Assert(PropertyDrawers != null); FusionEditorLog.Assert(PropertyDrawers.Count > 0); return PropertyDrawers[0].InvokeGetPropertyHeightInternal(property, label); } protected virtual float GetPropertyHeightInternal(SerializedProperty property, GUIContent label) { FusionEditorLog.Assert(MainDrawer != null); return MainDrawer.InvokeGetPropertyHeightOnNextDrawer(this, property, label); } protected virtual void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { TraceField($"OnGUIInternal({position}, {property.propertyPath}, {label})"); FusionEditorLog.Assert(MainDrawer != null); FusionEditorLog.Assert(_nestingLevel == 0, $"{property.propertyPath} {GetType().FullName}"); _nestingLevel++; try { MainDrawer.InvokeOnGUIOnNextDrawer(this, position, property, label); } finally { _nestingLevel--; } } private void InvokeOnGUIOnNextDrawer(DecoratingPropertyAttributeDrawer current, Rect position, SerializedProperty prop, GUIContent label) { FusionEditorLog.Assert(MainDrawer == this); var index = PropertyDrawers.IndexOf(current); if (index < PropertyDrawers.Count - 1) { PropertyDrawers[index + 1].InvokeOnGUIInternal(position, prop, label); } else { if (NextDrawer != null) { NextDrawer.OnGUI(position, prop, label); } else { FusionEditorGUI.ForwardPropertyField(position, prop, label, prop.IsArrayProperty() ? true : prop.isExpanded, _isLastDrawer); } } } private void InvokeOnGUIInternal(Rect position, SerializedProperty prop, GUIContent label) { if (prop.IsArrayElement() && this is INonApplicableOnArrayElements) { MainDrawer.InvokeOnGUIOnNextDrawer(this, position, prop, label); } else { OnGUIInternal(position, prop, label); } } private float InvokeGetPropertyHeightOnNextDrawer(DecoratingPropertyAttributeDrawer current, SerializedProperty prop, GUIContent label) { FusionEditorLog.Assert(MainDrawer == this); var index = PropertyDrawers.IndexOf(current); if (index < PropertyDrawers.Count - 1) { return PropertyDrawers[index + 1].InvokeGetPropertyHeightInternal(prop, label); } return NextDrawer?.GetPropertyHeight(prop, label) ?? EditorGUI.GetPropertyHeight(prop, label); } private float InvokeGetPropertyHeightInternal(SerializedProperty prop, GUIContent label) { if (prop.IsArrayElement() && this is INonApplicableOnArrayElements) { return MainDrawer.InvokeGetPropertyHeightOnNextDrawer(this, prop, label); } else { return GetPropertyHeightInternal(prop, label); } } protected virtual bool EnsureInitialized(SerializedProperty property) { if (MainDrawer != null || PropertyDrawers != null) { return false; } if (fieldInfo == null) { // this might happen if this drawer is created dynamically var field = UnityInternal.ScriptAttributeUtility.GetFieldInfoFromProperty(property, out _); FusionEditorLog.Assert(field != null, $"Could not find field for property {property.propertyPath} of type {property.serializedObject.targetObject.GetType().FullName} (I'm {GetType().FullName} {GetHashCode()})"); UnityInternal.PropertyDrawer.SetFieldInfo(this, field); } FusionEditorLog.Assert(attribute != null); FusionEditorLog.Assert(attribute is DecoratingPropertyAttribute, $"Expected attribute to be of type {nameof(DecoratingPropertyAttribute)} but it's {attribute.GetType().FullName}"); PropertyDrawers = new List(); MainDrawer = this; NextDrawer = null; var isLastDrawer = false; var foundSelf = false; var fieldAttributes = fieldInfo != null ? UnityInternal.ScriptAttributeUtility.GetFieldAttributes(fieldInfo) : null; if (fieldAttributes != null) { FusionEditorLog.Assert(fieldAttributes.OrderBy(x => x.order).SequenceEqual(fieldAttributes), "Expected field attributes to be sorted"); FusionEditorLog.Assert(fieldAttributes.Count > 0); for (var i = 0; i < fieldAttributes.Count; ++i) { var fieldAttribute = fieldAttributes[i]; var attributeDrawerType = UnityInternal.ScriptAttributeUtility.GetDrawerTypeForPropertyAndType(property, fieldAttribute.GetType()); if (attributeDrawerType == null) { TraceField($"No drawer for {attributeDrawerType}"); continue; } if (attributeDrawerType == typeof(PropertyDrawerForArrayWorkaround)) { attributeDrawerType = PropertyDrawerForArrayWorkaround.GetDrawerType(fieldAttribute.GetType()); } if (attributeDrawerType.IsSubclassOf(typeof(DecoratorDrawer))) { // decorators are their own thing continue; } if (property.IsArrayElement() && attributeDrawerType.GetInterface(typeof(INonApplicableOnArrayElements).FullName) != null) { // skip drawers that are not meant to be used on array elements continue; } FusionEditorLog.Assert(attributeDrawerType.IsSubclassOf(typeof(PropertyDrawer))); if (fieldAttribute.Equals(attribute)) { // self PropertyDrawers.Add(this); FusionEditorLog.Assert(foundSelf == false); foundSelf = true; isLastDrawer = true; TraceField($"Found self at {i} ({this})"); continue; } isLastDrawer = false; } } if (!foundSelf) { TraceField("Force-adding self"); PropertyDrawers.Add(this); } if (NextDrawer == null && isLastDrawer && fieldInfo != null) { // try creating type drawer instead var fieldType = fieldInfo.FieldType; if (property.IsArrayElement()) { fieldType = fieldType.GetUnityLeafType(); } var typeDrawerType = UnityInternal.ScriptAttributeUtility.GetDrawerTypeForPropertyAndType(property, fieldType); if (typeDrawerType != null) { var drawer = (PropertyDrawer)Activator.CreateInstance(typeDrawerType); UnityInternal.PropertyDrawer.SetFieldInfo(drawer, fieldInfo); TraceField($"Found final drawer is type drawer ({drawer})"); NextDrawer = drawer; } } if (isLastDrawer) { _isLastDrawer = true; } return true; } internal void InitInjected(PropertyDrawer next) { MainDrawer = this; PropertyDrawers = new List { this }; NextDrawer = next; } public PropertyDrawer GetNextDrawer(SerializedProperty property) { if (NextDrawer != null) { return NextDrawer; } var handler = UnityInternal.ScriptAttributeUtility.propertyHandlerCache.GetHandler(property); var drawers = handler.m_PropertyDrawers; var index = drawers.IndexOf(this); if (index >= 0 && index < drawers.Count - 1) { return drawers[index + 1]; } return null; } [Conditional("FUSION_EDITOR_TRACE")] private void TraceField(string message) { FusionEditorLog.TraceInspector($"[{GetType().FullName}] [{GetHashCode():X8}] [{fieldInfo?.DeclaringType.Name}.{fieldInfo?.Name}] {message}"); } } } #endregion #region DisplayAsEnumAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(DisplayAsEnumAttribute))] internal class DisplayAsEnumAttributeDrawer : PropertyDrawerWithErrorHandling { private EnumDrawer _enumDrawer; private Dictionary<(Type, string), Func> _cachedGetters = new Dictionary<(Type, string), Func>(); protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var attr = (DisplayAsEnumAttribute)attribute; var enumType = attr.EnumType; if (enumType == null && !string.IsNullOrEmpty(attr.EnumTypeMemberName)) { var objType = property.serializedObject.targetObject.GetType(); if (!_cachedGetters.TryGetValue((objType, attr.EnumTypeMemberName), out var getter)) { // maybe this is a top-level property then and we can use reflection? if (property.depth != 0) { FusionEditorLog.ErrorInspector($"Can't get enum type for {property.propertyPath}: non-SerializedProperty checks only work for top-level properties"); } else { try { getter = objType.CreateGetter(attr.EnumTypeMemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy); } catch (Exception e) { FusionEditorLog.ErrorInspector($"Can't get enum type for {property.propertyPath}: unable to create getter for {attr.EnumTypeMemberName} with exception {e}"); } } _cachedGetters.Add((objType, attr.EnumTypeMemberName), getter); } enumType = getter(property.serializedObject.targetObject); } using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out var valueRect)) { if (enumType == null) { SetError($"Unable to get enum type for {property.propertyPath}"); } else if (!enumType.IsEnum) { SetError($"Type {enumType} is not an enum type"); } else { ClearError(); _enumDrawer.Draw(valueRect, property, enumType, true); } } } } } #endregion #region DisplayNameAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; //[CustomPropertyDrawer(typeof(DisplayNameAttribute))] internal class DisplayNameAttributeDrawer : DecoratingPropertyAttributeDrawer, INonApplicableOnArrayElements { private GUIContent _label = new GUIContent(); protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { if (((DisplayNameAttribute)attribute).Name == null) { base.OnGUIInternal(position, property, label); return; } if (label.text == string.Empty && label.image == null || property.IsArrayElement()) { base.OnGUIInternal(position, property, label); return; } _label.text = ((DisplayNameAttribute)attribute).Name; _label.image = label.image; _label.tooltip = label.tooltip; base.OnGUIInternal(position, property, _label); } #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, DisplayNameAttribute attribute) { return new[] { new Sirenix.OdinInspector.LabelTextAttribute(attribute.Name) }; } #endif } [CustomPropertyDrawer(typeof(DisplayNameAttribute))] [RedirectCustomPropertyDrawer(typeof(DisplayNameAttribute), typeof(DisplayNameAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region DoIfAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; internal abstract partial class DoIfAttributeDrawer : DecoratingPropertyAttributeDrawer, INonApplicableOnArrayElements { private static Dictionary<(Type, string), Func> _cachedGetters = new Dictionary<(Type, string), Func>(); internal static bool CheckDraw(DoIfAttributeBase doIf, SerializedObject serializedObject) { var compareProperty = serializedObject.FindProperty(doIf.ConditionMember); if (compareProperty != null) { return CheckProperty(doIf, compareProperty); } return CheckGetter(doIf, serializedObject, 0, string.Empty) == true; } internal static bool CheckDraw(DoIfAttributeBase doIf, SerializedProperty property) { var compareProperty = property.depth < 0 ? property.FindPropertyRelative(doIf.ConditionMember) : property.FindPropertyRelativeToParent(doIf.ConditionMember); if (compareProperty != null) { return CheckProperty(doIf, compareProperty); } return CheckGetter(doIf, property.serializedObject, property.depth, property.propertyPath) == true; } private static bool CheckProperty(DoIfAttributeBase doIf, SerializedProperty compareProperty) { switch (compareProperty.propertyType) { case SerializedPropertyType.Boolean: case SerializedPropertyType.Integer: case SerializedPropertyType.Enum: case SerializedPropertyType.Character: return CheckCondition(doIf, compareProperty.longValue); case SerializedPropertyType.ObjectReference: return CheckCondition(doIf, compareProperty.objectReferenceInstanceIDValue); case SerializedPropertyType.Float: return CheckCondition(doIf, compareProperty.doubleValue); default: FusionEditorLog.ErrorInspector($"Can't check condition for {compareProperty.propertyPath}: unsupported property type {compareProperty.propertyType}"); return true; } } private static bool? CheckGetter(DoIfAttributeBase doIf, SerializedObject serializedObject, int depth, string referencePath) { var objType = serializedObject.targetObject.GetType(); if (!_cachedGetters.TryGetValue((objType, doIf.ConditionMember), out var getter)) { // maybe this is a top-level property then and we can use reflection? if (depth != 0) { if (doIf.ErrorOnConditionMemberNotFound) { FusionEditorLog.ErrorInspector($"Can't check condition for {referencePath}: non-SerializedProperty checks only work for top-level properties (depth:{depth}, conditionMember:{doIf.ConditionMember})"); } } else { try { getter = objType.CreateGetter(doIf.ConditionMember, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy); } catch (Exception e) { if (doIf.ErrorOnConditionMemberNotFound) { FusionEditorLog.ErrorInspector($"Can't check condition for {referencePath}: unable to create getter for {doIf.ConditionMember} with exception {e}"); } } } _cachedGetters.Add((objType, doIf.ConditionMember), getter); } if (getter != null) { bool? result = null; foreach (var target in serializedObject.targetObjects) { bool targetResult = CheckCondition(doIf, getter(target)); if (result.HasValue && result.Value != targetResult) { return null; } else { result = targetResult; } } return result; } else { return true; } } public static bool CheckCondition(DoIfAttributeBase attribute, double value) { if (!attribute._isDouble) throw new InvalidOperationException(); var doubleValue = attribute._doubleValue; switch (attribute.Compare) { case CompareOperator.Equal: return value == doubleValue; case CompareOperator.NotEqual: return value != doubleValue; case CompareOperator.Less: return value < doubleValue; case CompareOperator.LessOrEqual: return value <= doubleValue; case CompareOperator.GreaterOrEqual: return value >= doubleValue; case CompareOperator.Greater: return value > doubleValue; case CompareOperator.NotZero: return value != 0; case CompareOperator.IsZero: return value == 0; case CompareOperator.BitwiseAndNotEqualZero: throw new NotSupportedException(); default: throw new ArgumentOutOfRangeException(); } } public static bool CheckCondition(DoIfAttributeBase attribute, long value) { if (attribute._isDouble) throw new InvalidOperationException(); var _longValue = attribute._longValue; switch (attribute.Compare) { case CompareOperator.Equal: return value == _longValue; case CompareOperator.NotEqual: return value != _longValue; case CompareOperator.Less: return value < _longValue; case CompareOperator.LessOrEqual: return value <= _longValue; case CompareOperator.GreaterOrEqual: return value >= _longValue; case CompareOperator.Greater: return value > _longValue; case CompareOperator.NotZero: return value != 0; case CompareOperator.IsZero: return value == 0; case CompareOperator.BitwiseAndNotEqualZero: return (value & _longValue) != 0; default: throw new ArgumentOutOfRangeException(); } } public static bool CheckCondition(DoIfAttributeBase attribute, object value) { if (attribute._isDouble) { double converted = 0.0; if (value != null) { if (value is UnityEngine.Object o && !o) { // treat as 0 } else if (value.GetType().IsValueType) { converted = Convert.ToDouble(value); } else { converted = 1.0; } } return CheckCondition(attribute, converted); } else { long converted = 0; if (value != null) { if (value is UnityEngine.Object o && !o) { // treat as 0 } else if (value.GetType().IsValueType) { converted = Convert.ToInt64(value); } else { converted = 1; } } return CheckCondition(attribute, converted); } } } } #endregion #region DrawIfAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; internal partial class DrawIfAttributeDrawer : DoIfAttributeDrawer { public DrawIfAttribute Attribute => (DrawIfAttribute)attribute; protected override float GetPropertyHeightInternal(SerializedProperty property, GUIContent label) { if (Attribute.Mode == DrawIfMode.ReadOnly || CheckDraw(Attribute, property)) { return base.GetPropertyHeightInternal(property, label); } return -EditorGUIUtility.standardVerticalSpacing; } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var readOnly = Attribute.Mode == DrawIfMode.ReadOnly; var draw = CheckDraw(Attribute, property); if (readOnly || draw) { EditorGUI.BeginDisabledGroup(!draw); base.OnGUIInternal(position, property, label); EditorGUI.EndDisabledGroup(); } } } [CustomPropertyDrawer(typeof(DrawIfAttribute))] [RedirectCustomPropertyDrawer(typeof(DrawIfAttribute), typeof(DrawIfAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region DrawInlineAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(DrawInlineAttribute))] [FusionPropertyDrawerMeta(HasFoldout = false)] internal partial class DrawInlineAttributeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); foreach (var childProperty in property.GetChildren()) { position.height = EditorGUI.GetPropertyHeight(childProperty, true); EditorGUI.PropertyField(position, childProperty, true); position.y += position.height + EditorGUIUtility.standardVerticalSpacing; } EditorGUI.EndProperty(); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { float height = 0f; foreach (var childProperty in property.GetChildren()) { height += EditorGUI.GetPropertyHeight(childProperty, true) + EditorGUIUtility.standardVerticalSpacing; } height -= EditorGUIUtility.standardVerticalSpacing; return height; } } } #endregion #region ErrorIfAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; internal partial class ErrorIfAttributeDrawer : MessageIfDrawerBase { private new ErrorIfAttribute Attribute => (ErrorIfAttribute)attribute; protected override bool IsBox => Attribute.AsBox; protected override string Message => Attribute.Message; protected override MessageType MessageType => MessageType.Error; override protected Color InlineBoxColor => FusionEditorSkin.ErrorInlineBoxColor; protected override Texture MessageIcon => FusionEditorSkin.ErrorIcon; } [CustomPropertyDrawer(typeof(ErrorIfAttribute))] [RedirectCustomPropertyDrawer(typeof(ErrorIfAttribute), typeof(ErrorIfAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region ExpandableEnumAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(ExpandableEnumAttribute))] internal class ExpandableEnumAttributeDrawer : PropertyDrawerWithErrorHandling { private const float ToggleIndent = 5; private readonly GUIContent[] _gridOptions = new[] { new GUIContent("Nothing"), new GUIContent("Everything") }; private EnumDrawer _enumDrawer; private readonly LazyGUIStyle _buttonStyle = LazyGUIStyle.Create(_ => new GUIStyle(EditorStyles.miniButton) { fontSize = EditorStyles.miniButton.fontSize - 1 }); private new ExpandableEnumAttribute attribute => (ExpandableEnumAttribute)base.attribute; protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { bool wasExpanded = attribute.AlwaysExpanded || property.isExpanded; var rowRect = new Rect(position) { height = EditorGUIUtility.singleLineHeight, }; using (new FusionEditorGUI.PropertyScope(position, label, property)) { var valueRect = EditorGUI.PrefixLabel(rowRect, label); bool isEnum = property.propertyType == SerializedPropertyType.Enum; var maskProperty = isEnum ? property : property.FindPropertyRelative("Mask").FindPropertyRelative("values"); Mask256 rawValue; if (isEnum) { rawValue = new Mask256(maskProperty.longValue); } else { rawValue = new Mask256( maskProperty.GetFixedBufferElementAtIndex(0).longValue, maskProperty.GetFixedBufferElementAtIndex(1).longValue, maskProperty.GetFixedBufferElementAtIndex(2).longValue, maskProperty.GetFixedBufferElementAtIndex(3).longValue ); } var foldoutRect = new Rect(valueRect) { width = FusionEditorGUI.FoldoutWidth }; valueRect.xMin += foldoutRect.width; EditorGUI.BeginChangeCheck(); if (wasExpanded) { if (_enumDrawer.IsFlags && attribute.ShowFlagsButtons) { int gridValue = -1; if (rawValue.IsNothing()) { // nothing gridValue = 0; } else if (Equals(_enumDrawer.BitMask & rawValue, _enumDrawer.BitMask)) { var test = _enumDrawer.BitMask & rawValue; if (Equals(test, _enumDrawer.BitMask)) // everything gridValue = 1; } // traverse values in reverse; make sure the first alias is used in case there are multiple if (isEnum) { for (int i = _enumDrawer.Values.Length; i-- > 0;) { if (_enumDrawer.Values[i] == 0) { _gridOptions[0].text = _enumDrawer.Names[i]; } else if ( _enumDrawer.Values[i] == _enumDrawer.BitMask[0]) { // Unity's drawer does not replace "Everything" _gridOptions[1].text = _enumDrawer.Names[i]; } } } var gridSelection = GUI.SelectionGrid(valueRect, gridValue, _gridOptions, _gridOptions.Length, _buttonStyle); if (gridSelection != gridValue) { if (gridSelection == 0) { rawValue = default; } else if (gridSelection == 1) { rawValue = _enumDrawer.BitMask; } } } else { // draw a dummy field to consume the prefix EditorGUI.LabelField(valueRect, GUIContent.none); } } else { if (isEnum) { var enumValue = (Enum)Enum.ToObject(_enumDrawer.EnumType, rawValue[0]); if (_enumDrawer.IsFlags) { enumValue = EditorGUI.EnumFlagsField(valueRect, enumValue); } else { enumValue = EditorGUI.EnumPopup(valueRect, enumValue); } rawValue[0] = Convert.ToInt64(enumValue); } else { // Droplist for FieldsMask _enumDrawer.Draw(valueRect, maskProperty, fieldInfo.FieldType, false); } } if (EditorGUI.EndChangeCheck()) { if (isEnum) { maskProperty.longValue = rawValue[0]; } else { maskProperty.GetFixedBufferElementAtIndex(0).longValue = rawValue[0]; maskProperty.GetFixedBufferElementAtIndex(1).longValue = rawValue[1]; maskProperty.GetFixedBufferElementAtIndex(2).longValue = rawValue[2]; maskProperty.GetFixedBufferElementAtIndex(3).longValue = rawValue[3]; } property.serializedObject.ApplyModifiedProperties(); } if (!attribute.AlwaysExpanded) { using (new FusionEditorGUI.EnabledScope(true)) { property.isExpanded = EditorGUI.Toggle(foldoutRect, wasExpanded, EditorStyles.foldout); } } if (wasExpanded) { if (Event.current.type == EventType.Repaint) { EditorStyles.helpBox.Draw(new Rect(position) { yMin = rowRect.yMax }, GUIContent.none, false, false, false, false); } EditorGUI.BeginChangeCheck(); rowRect.xMin += ToggleIndent; for (int i = 0; i < _enumDrawer.Values.Length; ++i) { if (_enumDrawer.IsFlags && _enumDrawer.Values[i].IsNothing()) { continue; } rowRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; var toggleRect = rowRect; var buttonRect = new Rect(); if (attribute.ShowInlineHelp) { // move the button to keep it in the box buttonRect = FusionEditorGUI.GetInlineHelpButtonRect(rowRect); toggleRect.xMin += buttonRect.width + 0; buttonRect.x += buttonRect.width - 3; } bool wasSelected = _enumDrawer.IsFlags ? Equals(rawValue & _enumDrawer.Values[i], _enumDrawer.Values[i]) : Equals(rawValue, _enumDrawer.Values[i]); if (EditorGUI.ToggleLeft(toggleRect, _enumDrawer.Names[i], wasSelected) != wasSelected) { if (_enumDrawer.IsFlags) { if (wasSelected) { rawValue &= ~_enumDrawer.Values[i]; } else { rawValue |= _enumDrawer.Values[i]; } } else if (!wasSelected) { rawValue = _enumDrawer.Values[i]; } } if (attribute.ShowInlineHelp) { var helpContent = FusionCodeDoc.FindEntry(_enumDrawer.Fields[i], false); if (helpContent != null) { var helpPath = GetHelpPath(property, _enumDrawer.Fields[i]); var wasHelpExpanded = FusionEditorGUI.IsHelpExpanded(this, helpPath); if (wasHelpExpanded) { var helpSize = FusionEditorGUI.GetInlineBoxSize(helpContent); var helpRect = rowRect; helpRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; helpRect.height = helpSize.y; rowRect.y += helpSize.y; FusionEditorGUI.DrawInlineBoxUnderProperty(helpContent, helpRect, FusionEditorSkin.HelpInlineBoxColor, true); } buttonRect.x += buttonRect.width; if (FusionEditorGUI.DrawInlineHelpButton(buttonRect, wasHelpExpanded, doButton: true, doIcon: true)) { FusionEditorGUI.SetHelpExpanded(this, helpPath, !wasHelpExpanded); } } } } if (EditorGUI.EndChangeCheck()) { if (isEnum) { maskProperty.longValue = rawValue[0]; } else { maskProperty.GetFixedBufferElementAtIndex(0).longValue = rawValue[0]; maskProperty.GetFixedBufferElementAtIndex(1).longValue = rawValue[1]; maskProperty.GetFixedBufferElementAtIndex(2).longValue = rawValue[2]; maskProperty.GetFixedBufferElementAtIndex(3).longValue = rawValue[3]; } property.serializedObject.ApplyModifiedProperties(); } } } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { var enumType = property.propertyType == SerializedPropertyType.Enum ? fieldInfo.FieldType.GetUnityLeafType() : fieldInfo.FieldType; _enumDrawer.EnsureInitialized(enumType, attribute.ShowInlineHelp); int rowCount = 0; float height; var forceExpand = attribute.AlwaysExpanded; var showHelp = attribute.ShowInlineHelp; if (forceExpand || property.isExpanded) { if (_enumDrawer.IsFlags) { foreach (var value in _enumDrawer.Values) { if (value.IsNothing()) { continue; } ++rowCount; } } else { rowCount = _enumDrawer.Values.Length; } height = (rowCount + 1) * (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing); if (showHelp) { foreach (var field in _enumDrawer.Fields) { if (FusionEditorGUI.IsHelpExpanded(this, GetHelpPath(property, field))) { var helpContent = FusionCodeDoc.FindEntry(field, false); if (helpContent != null) { height += FusionEditorGUI.GetInlineBoxSize(helpContent).y; } } } } } else { height = EditorGUIUtility.singleLineHeight; } return height; } private static string GetHelpPath(SerializedProperty property, FieldInfo field) { return property.propertyPath + "/" + field.Name; } } } #endregion #region FieldEditorButtonAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Reflection; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; internal partial class FieldEditorButtonAttributeDrawer : DecoratingPropertyAttributeDrawer { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var propertyPosition = position; propertyPosition.height -= EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; base.OnGUIInternal(propertyPosition, property, label); var buttonPosition = position; buttonPosition.yMin = position.yMax - EditorGUIUtility.singleLineHeight; var attribute = (FieldEditorButtonAttribute)this.attribute; var targetObjects = property.serializedObject.targetObjects; var targetObjectType = property.serializedObject.targetObject.GetType(); if (DrawButton(buttonPosition, attribute, targetObjectType, targetObjects)) { property.serializedObject.Update(); property.serializedObject.ApplyModifiedProperties(); } } private static bool DrawButton(Rect buttonPosition, FieldEditorButtonAttribute attribute, Type targetObjectType, Object[] targetObjects) { using (new EditorGUI.DisabledGroupScope(!attribute.AllowMultipleTargets && targetObjects.Length > 1)) { if (GUI.Button(buttonPosition, attribute.Label, EditorStyles.miniButton)) { var targetMethod = targetObjectType.GetMethod(attribute.TargetMethod, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy); if (targetMethod == null) { FusionEditorLog.ErrorInspector($"Unable to find method {attribute.TargetMethod} on type {targetObjectType}"); } else { if (targetMethod.IsStatic) { targetMethod.Invoke(null, null); } else { foreach (var targetObject in targetObjects) { targetMethod.Invoke(targetObject, null); } } return true; } } return false; } } protected override float GetPropertyHeightInternal(SerializedProperty property, GUIContent label) { return base.GetPropertyHeightInternal(property, label) + EditorGUIUtility.standardVerticalSpacing + EditorGUIUtility.singleLineHeight; } } [CustomPropertyDrawer(typeof(FieldEditorButtonAttribute))] [RedirectCustomPropertyDrawer(typeof(FieldEditorButtonAttribute), typeof(FieldEditorButtonAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region HideArrayElementLabelAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(HideArrayElementLabelAttribute))] partial class HideArrayElementLabelAttributeDrawer : DecoratingPropertyAttributeDrawer { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { if (property.IsArrayElement()) { label = GUIContent.none; } base.OnGUIInternal(position, property, label); } } } #endregion #region InlineHelpAttributeDrawer.cs namespace Fusion.Editor { using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; //[CustomPropertyDrawer(typeof(InlineHelpAttribute))] internal partial class InlineHelpAttributeDrawer : DecoratingPropertyAttributeDrawer, INonApplicableOnArrayElements { private bool _initialized; private GUIContent _helpContent; private GUIContent _labelContent; protected new InlineHelpAttribute attribute => (InlineHelpAttribute)base.attribute; protected override float GetPropertyHeightInternal(SerializedProperty property, GUIContent label) { var height = base.GetPropertyHeightInternal(property, label); if (height <= 0) { return height; } if (FusionEditorGUI.IsHelpExpanded(this, property.propertyPath)) { var helpContent = GetHelpContent(property); if (helpContent != null) { height += FusionEditorGUI.GetInlineBoxSize(helpContent).y; } } return height; } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var helpContent = GetHelpContent(property); if (position.height <= 0 || helpContent == null) { // ignore base.OnGUIInternal(position, property, label); return; } var nextDrawer = GetNextDrawer(property); var hasFoldout = HasFoldout(nextDrawer, property); using (new FusionEditorGUI.GUIContentScope(label)) { var (wasExpanded, buttonRect) = DrawInlineHelpBeforeProperty(label, helpContent, position, property.propertyPath, EditorGUI.indentLevel, hasFoldout, this); var propertyRect = position; if (wasExpanded) { propertyRect.height -= FusionEditorGUI.GetInlineBoxSize(helpContent).y; } base.OnGUIInternal(propertyRect, property, label); DrawInlineHelpAfterProperty(buttonRect, wasExpanded, helpContent, position); } } private GUIContent GetHelpContent(SerializedProperty property) { if (_initialized) { return _helpContent; } _initialized = true; if (property.IsArrayElement()) { return null; } if (fieldInfo == null) { return null; } _helpContent = FusionCodeDoc.FindEntry(fieldInfo, attribute.ShowTypeHelp); return _helpContent; } private bool HasFoldout(PropertyDrawer nextDrawer, SerializedProperty property) { var drawerMeta = nextDrawer?.GetType().GetCustomAttribute(); if (drawerMeta != null) { return drawerMeta.HasFoldout; } if (property.IsArrayProperty()) { return true; } if (property.propertyType == SerializedPropertyType.Generic) { return true; } return false; } public static (bool expanded, Rect buttonRect) DrawInlineHelpBeforeProperty(GUIContent label, GUIContent helpContent, Rect propertyRect, string propertyPath, int depth, bool hasFoldout, object context, bool drawHelp = false) { if (label != null) { if (!string.IsNullOrEmpty(label.tooltip)) { label.tooltip += "\n\n"; } label.tooltip += helpContent.tooltip; } if (propertyRect.width > 1 && propertyRect.height > 1) { var buttonRect = FusionEditorGUI.GetInlineHelpButtonRect(propertyRect, hasFoldout); if (depth == 0 && hasFoldout) { buttonRect.x = 16; if (label != null) { label.text = " " + label.text; } } var wasExpanded = FusionEditorGUI.IsHelpExpanded(context, propertyPath); if (FusionEditorGUI.DrawInlineHelpButton(buttonRect, wasExpanded, doButton: true, doIcon: false)) { FusionEditorGUI.SetHelpExpanded(context, propertyPath, !wasExpanded); } return (wasExpanded, buttonRect); } return default; } public static void DrawInlineHelpAfterProperty(Rect buttonRect, bool wasExpanded, GUIContent helpContent, Rect propertyRect) { if (buttonRect.width <= 0 && buttonRect.height <= 0) { return; } using (new FusionEditorGUI.EnabledScope(true)) { FusionEditorGUI.DrawInlineHelpButton(buttonRect, wasExpanded, doButton: false, doIcon: true); } if (!wasExpanded) { return; } FusionEditorGUI.DrawInlineBoxUnderProperty(helpContent, propertyRect, FusionEditorSkin.HelpInlineBoxColor, true); } } [CustomPropertyDrawer(typeof(InlineHelpAttribute))] [RedirectCustomPropertyDrawer(typeof(InlineHelpAttribute), typeof(InlineHelpAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region INonApplicableOnArrayElements.cs namespace Fusion.Editor { public interface INonApplicableOnArrayElements { } } #endregion #region LayerAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(LayerAttribute))] internal class LayerAttributeDrawer : PropertyDrawer { public override void OnGUI(Rect p, SerializedProperty prop, GUIContent label) { EditorGUI.BeginChangeCheck(); int value; using (new FusionEditorGUI.PropertyScope(p, label, prop)) using (new FusionEditorGUI.ShowMixedValueScope(prop.hasMultipleDifferentValues)) { value = EditorGUI.LayerField(p, label, prop.intValue); } if (EditorGUI.EndChangeCheck()) { prop.intValue = value; prop.serializedObject.ApplyModifiedProperties(); } } } } #endregion #region LayerMatrixAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; internal partial class LayerMatrixAttributeDrawer : PropertyDrawerWithErrorHandling, INonApplicableOnArrayElements { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out var valueRect)) { if (GUI.Button(valueRect, "Edit", EditorStyles.miniButton)) { PopupWindow.Show(valueRect, new LayerMatrixPopup(label?.text ?? property.displayName, (layerA, layerB) => { if (layerA >= property.arraySize) { return false; } return (property.GetArrayElementAtIndex(layerA).intValue & (1 << layerB)) != 0; }, (layerA, layerB, val) => { if (Mathf.Max(layerA, layerB) >= property.arraySize) { property.arraySize = Mathf.Max(layerA, layerB) + 1; } if (val) { property.GetArrayElementAtIndex(layerA).intValue |= (1 << layerB); property.GetArrayElementAtIndex(layerB).intValue |= (1 << layerA); } else { property.GetArrayElementAtIndex(layerA).intValue &= ~(1 << layerB); property.GetArrayElementAtIndex(layerB).intValue &= ~(1 << layerA); } property.serializedObject.ApplyModifiedProperties(); })); } } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } class LayerMatrixPopup : PopupWindowContent { private const int checkboxSize = 16; private const int margin = 30; private const int MaxLayers = 32; private readonly GUIContent _label; private readonly int _numLayers; private readonly float _labelWidth; private readonly UnityInternal.LayerMatrixGUI.GetValueFunc _getter; private readonly UnityInternal.LayerMatrixGUI.SetValueFunc _setter; public LayerMatrixPopup(string label, UnityInternal.LayerMatrixGUI.GetValueFunc getter, UnityInternal.LayerMatrixGUI.SetValueFunc setter) { _label = new GUIContent(label); _getter = getter; _setter = setter; _labelWidth = 110; _numLayers = 0; for (int i = 0; i < MaxLayers; i++) { string layerName = LayerMask.LayerToName(i); if (string.IsNullOrEmpty(layerName)) { continue; } _numLayers++; _labelWidth = Mathf.Max(_labelWidth, GUI.skin.label.CalcSize(new GUIContent(layerName)).x); } } public override void OnGUI(Rect rect) { GUILayout.BeginArea(rect); UnityInternal.LayerMatrixGUI.Draw(_label, _getter, _setter); GUILayout.EndArea(); } public override Vector2 GetWindowSize() { int matrixWidth = checkboxSize * _numLayers; float width = matrixWidth + _labelWidth + margin * 2; float heigth = matrixWidth + _labelWidth + 15 + FusionEditorGUI.GetLinesHeight(3); return new Vector2(Mathf.Max(width, 350), heigth); } } } [CustomPropertyDrawer(typeof(LayerMatrixAttribute))] [RedirectCustomPropertyDrawer(typeof(LayerMatrixAttribute), typeof(LayerMatrixAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region MaxStringByteCountAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(MaxStringByteCountAttribute))] internal class MaxStringByteCountAttributeDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var attribute = (MaxStringByteCountAttribute)this.attribute; var encoding = System.Text.Encoding.GetEncoding(attribute.Encoding); var byteCount = encoding.GetByteCount(property.stringValue); using (new FusionEditorGUI.PropertyScope(position, label, property)) { FusionEditorGUI.ForwardPropertyField(position, property, label, true); } FusionEditorGUI.Overlay(position, $"({byteCount} B)"); if (byteCount > attribute.ByteCount) { FusionEditorGUI.Decorate(position, $"{attribute.Encoding} string max size ({attribute.ByteCount} B) exceeded: {byteCount} B", MessageType.Error, hasLabel: true); } } } } #endregion #region MessageIfDrawerBase.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; internal abstract class MessageIfDrawerBase : DoIfAttributeDrawer { protected abstract bool IsBox { get; } protected abstract string Message { get; } protected abstract MessageType MessageType { get; } protected abstract Color InlineBoxColor { get; } protected abstract Texture MessageIcon { get; } public DoIfAttributeBase Attribute => (DoIfAttributeBase)attribute; private GUIContent _messageContent; private GUIContent MessageContent { get { if (_messageContent == null) { _messageContent = new GUIContent(Message, MessageIcon, Message); } return _messageContent; } } protected override float GetPropertyHeightInternal(SerializedProperty property, GUIContent label) { var height = base.GetPropertyHeightInternal(property, label); if (IsBox) { if (CheckDraw(Attribute, property)) { float extra = CalcBoxHeight(); height += extra; } } return height; } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { if (!CheckDraw(Attribute, property)) { base.OnGUIInternal(position, property, label); } else { if (!IsBox) { var decorateRect = position; decorateRect.height = EditorGUIUtility.singleLineHeight; decorateRect.xMin += EditorGUIUtility.labelWidth; // TODO: should the border be resized for arrays? // if (property.IsArrayProperty()) { // decorateRect.xMin = decorateRect.xMax - 48f; // } FusionEditorGUI.AppendTooltip(MessageContent.text, ref label); base.OnGUIInternal(position, property, label); FusionEditorGUI.Decorate(decorateRect, MessageContent.text, MessageType); } else { position = FusionEditorGUI.DrawInlineBoxUnderProperty(MessageContent, position, InlineBoxColor); base.OnGUIInternal(position, property, label); //position.y += position.height; //position.height = extra; //EditorGUI.HelpBox(position, MessageContent.text, MessageType); } } } private float CalcBoxHeight() { // const float SCROLL_WIDTH = 16f; // const float LEFT_HELP_INDENT = 8f; // // var width = UnityInternal.EditorGUIUtility.contextWidth - /*InlineHelpStyle.MarginOuter -*/ SCROLL_WIDTH - LEFT_HELP_INDENT; // return EditorStyles.helpBox.CalcHeight(MessageContent, width); return FusionEditorGUI.GetInlineBoxSize(MessageContent).y; } } } #endregion #region PropertyDrawerForArrayWorkaround.cs //#define FUSION_EDITOR_TRACE namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; internal partial class PropertyDrawerForArrayWorkaround : DecoratorDrawer { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] internal class RedirectCustomPropertyDrawerAttribute : Attribute { public RedirectCustomPropertyDrawerAttribute(Type attributeType, Type drawerType) { AttributeType = attributeType; DrawerType = drawerType; } public Type AttributeType { get; } public Type DrawerType { get; } } private static Dictionary _attributeToDrawer = typeof(PropertyDrawerForArrayWorkaround) .GetCustomAttributes() .ToDictionary(x => x.AttributeType, x => x.DrawerType); private UnityInternal.PropertyHandler _handler; private PropertyDrawer _drawer; private bool _initialized; public PropertyDrawerForArrayWorkaround() { _handler = UnityInternal.ScriptAttributeUtility.nextHandler; } public override float GetHeight() { if (!_initialized) { _initialized = true; if (!_attributeToDrawer.TryGetValue(attribute.GetType(), out var drawerType)) { FusionEditorLog.ErrorInspector($"No drawer for {attribute.GetType()}"); } else if (_handler.decoratorDrawers?.Contains(this) != true) { FusionEditorLog.Warn($"Unable to forward to {drawerType}."); } else { var drawer = (PropertyDrawer)Activator.CreateInstance(drawerType); UnityInternal.PropertyDrawer.SetAttribute(drawer, attribute); // if (_handler.decoratorDrawers.Contains(this)) { // } if (_handler.m_PropertyDrawers == null) { _handler.m_PropertyDrawers = new List(); } var insertPosition = _handler.m_PropertyDrawers.TakeWhile(x => x.attribute != null && x.attribute.order < attribute.order) .Count(); FusionEditorLog.Trace($"Inserting {drawerType} at {insertPosition}"); _handler.m_PropertyDrawers.Insert(insertPosition, drawer); } } return 0; } public static Type GetDrawerType(Type attributeDrawerType) { return _attributeToDrawer[attributeDrawerType]; } } // [CustomPropertyDrawer(typeof(Attrib))] // public class DummyDrawer : ForwardingPropertyDrawer { // public class Attrib : PropertyAttribute { // } // // public DummyDrawer() { // //ReadOnlyAttribute // } // // protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { // base.OnGUIInternal(position, property, label); // } // // protected override float GetPropertyHeightInternal(SerializedProperty property, GUIContent label) { // return base.GetPropertyHeightInternal(property, label); // } // } // // [CustomPropertyDrawer(typeof(Attrib))] // public class FooPropertyDrawer : PropertyDrawer { // public class Attrib : PropertyAttribute { // public PropertyAttribute OtherAttribute; // public Type OtherDrawerType; // } // // private PropertyDrawer _otherDrawer; // // private void EnsureOtherDrawer(SerializedProperty property) { // if (_otherDrawer == null) { // var attrib = (Attrib)attribute; // _otherDrawer = (PropertyDrawer)Activator.CreateInstance(attrib.OtherDrawerType); // UnityInternal.PropertyDrawer.SetAttribute(_otherDrawer, attrib.OtherAttribute); // UnityInternal.PropertyDrawer.SetFieldInfo(_otherDrawer, fieldInfo); // } // } // // public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { // EnsureOtherDrawer(property); // return _otherDrawer.GetPropertyHeight(property, label); // } // // public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // EnsureOtherDrawer(property); // _otherDrawer.OnGUI(position, property, label); // } // } } #endregion #region PropertyDrawerWithErrorHandling.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; internal abstract class PropertyDrawerWithErrorHandling : PropertyDrawer { private SerializedProperty _currentProperty; private readonly Dictionary _errors = new(); private bool _hadError; private string _info; public float IconOffset; public sealed override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { FusionEditorLog.Assert(_currentProperty == null); var decoration = GetDecoration(property); if (decoration != null) { DrawDecoration(position, decoration.Value, label != GUIContent.none, true, false); } _currentProperty = property; _hadError = false; _info = null; EditorGUI.BeginChangeCheck(); try { OnGUIInternal(position, property, label); } catch (ExitGUIException) { // pass through } catch (Exception ex) { SetError(ex.ToString()); } finally { // if there was a change but no error clear if (EditorGUI.EndChangeCheck() && !_hadError) { ClearError(); } _currentProperty = null; } if (decoration != null) { DrawDecoration(position, decoration.Value, label != GUIContent.none, false, true); } } private void DrawDecoration(Rect position, (string, MessageType, bool) decoration, bool hasLabel, bool drawButton = true, bool drawIcon = true) { var iconPosition = position; iconPosition.height = EditorGUIUtility.singleLineHeight; iconPosition.x -= IconOffset; FusionEditorGUI.Decorate(iconPosition, decoration.Item1, decoration.Item2, hasLabel, drawButton: drawButton, drawBorder: decoration.Item3); } private (string, MessageType, bool)? GetDecoration(SerializedProperty property) { if (_errors.TryGetValue(property.propertyPath, out var error)) { return (error.message, error.type, true); } if (_info != null) { return (_info, MessageType.Info, false); } return null; } protected abstract void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label); protected void ClearError() { ClearError(_currentProperty); } protected void ClearError(SerializedProperty property) { _hadError = false; _errors.Remove(property.propertyPath); } protected void ClearErrorIfLostFocus() { if (GUIUtility.keyboardControl != UnityInternal.EditorGUIUtility.LastControlID) { ClearError(); } } protected void SetError(string error) { _hadError = true; _errors[_currentProperty.propertyPath] = new Entry { message = error, type = MessageType.Error }; } protected void SetError(Exception error) { SetError(error.ToString()); } protected void SetWarning(string warning) { if (_errors.TryGetValue(_currentProperty.propertyPath, out var entry) && entry.type == MessageType.Error) { return; } _errors[_currentProperty.propertyPath] = new Entry { message = warning, type = MessageType.Warning }; } protected void SetInfo(string message) { if (_errors.TryGetValue(_currentProperty.propertyPath, out var entry) && entry.type == MessageType.Error || entry.type == MessageType.Warning ) { return; } _errors[_currentProperty.propertyPath] = new Entry { message = message, type = MessageType.Info }; } private struct Entry { public string message; public MessageType type; } } } #endregion #region RangeAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(RangeExAttribute))] internal class RangeAttributeDrawer : PropertyDrawer { const float FieldWidth = 130.0f; const float Spacing = 5.0f; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var attrib = (RangeExAttribute)this.attribute; var min = attrib.Min; var max = attrib.Max; int intValue = 0; float floatValue = 0.0f; if (property.propertyType == SerializedPropertyType.Float) { floatValue = property.floatValue; } else if (property.propertyType == SerializedPropertyType.Integer) { intValue = property.intValue; } else { EditorGUI.LabelField(position, label.text, "Use RangeEx with float or int."); return; } EditorGUI.BeginChangeCheck(); using (new FusionEditorGUI.PropertyScope(position, label, property)) { if (attrib.UseSlider) { if (!attrib.ClampMin || !attrib.ClampMax) { var sliderRect = new Rect(position) { xMin = position.xMin + EditorGUIUtility.labelWidth, xMax = position.xMax - FieldWidth - Spacing }; using (new FusionEditorGUI.LabelWidthScope(position.width - FieldWidth)) { if (property.propertyType == SerializedPropertyType.Float) { if (sliderRect.width > FieldWidth + Spacing) { using (new EditorGUI.IndentLevelScope(-EditorGUI.indentLevel)) { floatValue = GUI.HorizontalSlider(sliderRect, property.floatValue, (float)min, (float)max); } } floatValue = EditorGUI.FloatField(position, label, floatValue); } else { if (sliderRect.width > FieldWidth) { using (new EditorGUI.IndentLevelScope(-EditorGUI.indentLevel)) { intValue = Mathf.RoundToInt(GUI.HorizontalSlider(sliderRect, property.intValue, (float)min, (float)max)); } } intValue = EditorGUI.IntField(position, label, intValue); } } } else { var fieldWidth = EditorGUIUtility.fieldWidth; try { EditorGUIUtility.fieldWidth = FieldWidth; if (property.propertyType == SerializedPropertyType.Float) { floatValue = EditorGUI.Slider(position, label, property.floatValue, (float)min, (float)max); } else { intValue = EditorGUI.IntSlider(position, label, property.intValue, (int)min, (int)max); } } finally { EditorGUIUtility.fieldWidth = fieldWidth; } } } else { if (property.propertyType == SerializedPropertyType.Float) { floatValue = EditorGUI.FloatField(position, label, property.floatValue); } else { intValue = EditorGUI.IntField(position, label, property.intValue); } } } if (EditorGUI.EndChangeCheck()) { if (property.propertyType == SerializedPropertyType.Float) { property.floatValue = Clamp(floatValue, attrib); } else if (property.propertyType == SerializedPropertyType.Integer) { property.intValue = Clamp(intValue, attrib); } property.serializedObject.ApplyModifiedProperties(); } } private float Clamp(float value, RangeExAttribute attrib) { return Mathf.Clamp(value, attrib.ClampMin ? (float)attrib.Min : float.MinValue, attrib.ClampMax ? (float)attrib.Max : float.MaxValue); } private int Clamp(int value, RangeExAttribute attrib) { return Mathf.Clamp(value, attrib.ClampMin ? (int)attrib.Min : int.MinValue, attrib.ClampMax ? (int)attrib.Max : int.MaxValue); } } } #endregion #region ReadOnlyAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; internal partial class ReadOnlyAttributeDrawer : DecoratingPropertyAttributeDrawer, INonApplicableOnArrayElements { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var attribute = (ReadOnlyAttribute)this.attribute; bool isPlayMode = EditorApplication.isPlayingOrWillChangePlaymode; using (new EditorGUI.DisabledGroupScope(isPlayMode ? attribute.InPlayMode : attribute.InEditMode)) { base.OnGUIInternal(position, property, label); } } } [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] [RedirectCustomPropertyDrawer(typeof(ReadOnlyAttribute), typeof(ReadOnlyAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region ScenePathAttributeDrawer.cs namespace Fusion.Editor { using System.Linq; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(ScenePathAttribute))] internal class ScenePathAttributeDrawer : PropertyDrawerWithErrorHandling { private SceneAsset[] _allScenes; protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var oldScene = AssetDatabase.LoadAssetAtPath(property.stringValue); if (oldScene == null && !string.IsNullOrEmpty(property.stringValue)) { // well, maybe by name then? _allScenes = _allScenes ?? AssetDatabase.FindAssets("t:scene") .Select(x => AssetDatabase.GUIDToAssetPath(x)) .Select(x => AssetDatabase.LoadAssetAtPath(x)) .ToArray(); var matchedByName = _allScenes.Where(x => x.name == property.stringValue).ToList(); ; if (matchedByName.Count == 0) { SetError($"Scene not found: {property.stringValue}"); } else { oldScene = matchedByName[0]; if (matchedByName.Count > 1) { SetWarning("There are multiple scenes with this name"); } } } using (new FusionEditorGUI.PropertyScope(position, label, property)) { EditorGUI.BeginChangeCheck(); var newScene = EditorGUI.ObjectField(position, label, oldScene, typeof(SceneAsset), false) as SceneAsset; if (EditorGUI.EndChangeCheck()) { var assetPath = AssetDatabase.GetAssetPath(newScene); property.stringValue = assetPath; property.serializedObject.ApplyModifiedProperties(); ClearError(); } } } } } #endregion #region ScriptFieldDrawer.cs namespace Fusion.Editor { using System; using UnityEditor; using UnityEngine; internal class ScriptFieldDrawer : PropertyDrawer { private new ScriptHelpAttribute attribute => (ScriptHelpAttribute)base.attribute; public bool ForceHide = false; private bool _initialized; private GUIContent _helpContent; private GUIContent _headerContent; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (ForceHide || attribute?.Hide == true) { return; } if (attribute == null) { EditorGUI.PropertyField(position, property, label); return; } EnsureInitialized(property); var helpButtonRect = FusionEditorGUI.GetInlineHelpButtonRect(position, false); bool wasHelpExpanded = _helpContent != null && FusionEditorGUI.IsHelpExpanded(this, property.propertyPath); if (wasHelpExpanded) { position = FusionEditorGUI.DrawInlineBoxUnderProperty(_helpContent, position, FusionEditorSkin.HelpInlineBoxColor); } if (_helpContent != null) { using (new FusionEditorGUI.EnabledScope(true)) { if (FusionEditorGUI.DrawInlineHelpButton(helpButtonRect, wasHelpExpanded, true, false)) { FusionEditorGUI.SetHelpExpanded(this, property.propertyPath, !wasHelpExpanded); } } } if (attribute.Style == ScriptHeaderStyle.Unity) { EditorGUI.PropertyField(position, property, label); } else { using (new FusionEditorGUI.EnabledScope(true)) { if (attribute.BackColor != ScriptHeaderBackColor.None) { FusionEditorGUI.DrawScriptHeaderBackground(position, FusionEditorSkin.GetScriptHeaderColor(attribute.BackColor)); } var labelPosition = FusionEditorSkin.ScriptHeaderLabelStyle.margin.Remove(position); EditorGUIUtility.AddCursorRect(labelPosition, MouseCursor.Link); EditorGUI.LabelField(labelPosition, _headerContent, FusionEditorSkin.ScriptHeaderLabelStyle); var e = Event.current; if (e.type == EventType.MouseDown && position.Contains(e.mousePosition)) { if (e.clickCount == 1) { if (!string.IsNullOrEmpty(attribute.Url)) { Application.OpenURL(attribute.Url); } EditorGUIUtility.PingObject(property.objectReferenceValue); } else { AssetDatabase.OpenAsset(property.objectReferenceValue); } } FusionEditorGUI.DrawScriptHeaderIcon(position); } } if (_helpContent != null) { using (new FusionEditorGUI.EnabledScope(true)) { // paint over what the inspector has drawn FusionEditorGUI.DrawInlineHelpButton(helpButtonRect, wasHelpExpanded, false, true); } } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { if (ForceHide || attribute?.Hide == true) { return -EditorGUIUtility.standardVerticalSpacing; } if (attribute == null) { return EditorGUIUtility.singleLineHeight; } var height = EditorGUIUtility.singleLineHeight; if (FusionEditorGUI.IsHelpExpanded(this, property.propertyPath) && _helpContent != null) { height += FusionEditorGUI.GetInlineBoxSize(_helpContent).y; } return height; } private void EnsureInitialized(SerializedProperty property) { if (_initialized) { return; } _initialized = true; var type = property.serializedObject.targetObject.GetType(); _headerContent = new GUIContent(ObjectNames.NicifyVariableName(type.Name).ToUpper()); _helpContent = FusionCodeDoc.FindEntry(type); } } } #endregion #region SerializableTypeDrawer.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(SerializableType<>))] [CustomPropertyDrawer(typeof(SerializableType))] [CustomPropertyDrawer(typeof(SerializableTypeAttribute))] internal class SerializableTypeDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var attr = (SerializableTypeAttribute)attribute; SerializedProperty valueProperty; if (property.propertyType == SerializedPropertyType.String) { FusionEditorLog.Assert(attr != null); valueProperty = property; } else { FusionEditorLog.Assert(property.propertyType == SerializedPropertyType.Generic); valueProperty = property.FindPropertyRelativeOrThrow(nameof(SerializableType.AssemblyQualifiedName)); } var assemblyQualifiedName = valueProperty.stringValue; var baseType = typeof(object); var leafType = fieldInfo.FieldType.GetUnityLeafType(); if (leafType.IsGenericType && leafType.GetGenericTypeDefinition() == typeof(SerializableType<>)) { baseType = leafType.GetGenericArguments()[0]; } if (attr?.BaseType != null) { baseType = attr.BaseType; } position = EditorGUI.PrefixLabel(position, label); string content = "[None]"; if (!string.IsNullOrEmpty(assemblyQualifiedName)) { try { var type = Type.GetType(assemblyQualifiedName, true); content = type.FullName; } catch (Exception e) { SetError(e); content = assemblyQualifiedName; } } if (EditorGUI.DropdownButton(position, new GUIContent(content), FocusType.Keyboard)) { ClearError(); FusionEditorGUI.DisplayTypePickerMenu(position, baseType, t => { string typeName = string.Empty; if (t != null) { typeName = attr?.UseFullAssemblyQualifiedName == false ? SerializableType.GetShortAssemblyQualifiedName(t) : t.AssemblyQualifiedName; } valueProperty.stringValue = typeName; valueProperty.serializedObject.ApplyModifiedProperties(); }); } } } } #endregion #region SerializeReferenceTypePickerAttributeDrawer.cs namespace Fusion.Editor { using System.Linq; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(SerializeReferenceTypePickerAttribute))] partial class SerializeReferenceTypePickerAttributeDrawer : DecoratingPropertyAttributeDrawer { const string NullContent = "Null"; protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var attribute = (SerializeReferenceTypePickerAttribute)this.attribute; Rect pickerRect; if (label == GUIContent.none) { pickerRect = position; pickerRect.height = EditorGUIUtility.singleLineHeight; } else { pickerRect = EditorGUI.PrefixLabel(new Rect(position) { height = EditorGUIUtility.singleLineHeight }, FusionEditorGUI.WhitespaceContent); } object instance = property.managedReferenceValue; var instanceType = instance?.GetType(); if (EditorGUI.DropdownButton(pickerRect, new GUIContent(instanceType?.FullName ?? NullContent), FocusType.Keyboard)) { var types = attribute.Types; if (!types.Any()) { types = new[] { fieldInfo.FieldType.GetUnityLeafType() }; } FusionEditorGUI.DisplayTypePickerMenu(pickerRect, types, t => { if (t == null) { instance = null; } else if (t.IsInstanceOfType(instance)) { // do nothing return; } else { instance = System.Activator.CreateInstance(t); } property.managedReferenceValue = instance; property.serializedObject.ApplyModifiedProperties(); }, noneOptionLabel: NullContent, groupByNamespace: attribute.GroupTypesByNamespace, selectedType: instanceType); } base.OnGUIInternal(position, property, label); } } } #endregion #region ToggleLeftAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(ToggleLeftAttribute))] internal class ToggleLeftAttributeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); EditorGUI.BeginChangeCheck(); var val = EditorGUI.ToggleLeft(position, label, property.boolValue); if (EditorGUI.EndChangeCheck()) { property.boolValue = val; } EditorGUI.EndProperty(); } } } #endregion #region UnitAttributeDrawer.cs namespace Fusion.Editor { using System; using System.Reflection; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(UnitAttribute))] [FusionPropertyDrawerMeta(HandlesUnits = true)] internal partial class UnitAttributeDrawer : DecoratingPropertyAttributeDrawer { private GUIContent _label; private void EnsureInitialized() { if (_label == null) { _label = new GUIContent(UnitToLabel(((UnitAttribute)attribute).Unit)); } } protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { base.OnGUIInternal(position, property, label); // check if any of the next drawers handles the unit for (var nextDrawer = GetNextDrawer(property); nextDrawer != null; nextDrawer = (nextDrawer as DecoratingPropertyAttributeDrawer)?.GetNextDrawer(property)) { var meta = nextDrawer.GetType().GetCustomAttribute(); if (meta?.HandlesUnits == true) { return; } } EnsureInitialized(); var propertyType = property.propertyType; var isExpanded = property.isExpanded; DrawUnitOverlay(position, _label, propertyType, isExpanded); } public static void DrawUnitOverlay(Rect position, GUIContent label, SerializedPropertyType propertyType, bool isExpanded, bool odinStyle = false) { switch (propertyType) { case SerializedPropertyType.Vector2 when odinStyle: case SerializedPropertyType.Vector3 when odinStyle: case SerializedPropertyType.Vector4 when odinStyle: { var pos = position; int memberCount = (propertyType == SerializedPropertyType.Vector2) ? 2 : (propertyType == SerializedPropertyType.Vector3) ? 3 : 4; pos.xMin += EditorGUIUtility.labelWidth; pos.yMin = pos.yMax - EditorGUIUtility.singleLineHeight; pos.width /= memberCount; pos.height = EditorGUIUtility.singleLineHeight; for (int i = 0; i < memberCount; ++i) { FusionEditorGUI.Overlay(pos, label); pos.x += pos.width; } break; } case SerializedPropertyType.Vector2: case SerializedPropertyType.Vector3: { Rect pos = position; // vector properties get broken down into two lines when there's not enough space if (EditorGUIUtility.wideMode) { pos.xMin += EditorGUIUtility.labelWidth; pos.width /= 3; } else { pos.xMin += 12; pos.yMin = pos.yMax - EditorGUIUtility.singleLineHeight; pos.width /= (propertyType == SerializedPropertyType.Vector2) ? 2 : 3; } pos.height = EditorGUIUtility.singleLineHeight; FusionEditorGUI.Overlay(pos, label); pos.x += pos.width; FusionEditorGUI.Overlay(pos, label); if (propertyType == SerializedPropertyType.Vector3) { pos.x += pos.width; FusionEditorGUI.Overlay(pos, label); } break; } case SerializedPropertyType.Vector4: if (isExpanded) { Rect pos = position; pos.yMin = pos.yMax - 4 * EditorGUIUtility.singleLineHeight - 3 * EditorGUIUtility.standardVerticalSpacing; pos.height = EditorGUIUtility.singleLineHeight; for (int i = 0; i < 4; ++i) { FusionEditorGUI.Overlay(pos, label); pos.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; } } break; default: { var pos = position; pos.height = EditorGUIUtility.singleLineHeight; FusionEditorGUI.Overlay(pos, label); } break; } } public static string UnitToLabel(Units units) { switch (units) { case Units.None: return string.Empty; case Units.Ticks: return "ticks"; case Units.Seconds: return "s"; case Units.MilliSecs: return "ms"; case Units.Kilobytes: return "kB"; case Units.Megabytes: return "MB"; case Units.Normalized: return "normalized"; case Units.Multiplier: return "multiplier"; case Units.Percentage: return "%"; case Units.NormalizedPercentage: return "n%"; case Units.Degrees: return "\u00B0"; case Units.PerSecond: return "hz"; case Units.DegreesPerSecond: return "\u00B0/sec"; case Units.Radians: return "rad"; case Units.RadiansPerSecond: return "rad/s"; case Units.TicksPerSecond: return "ticks/s"; case Units.Units: return "units"; case Units.Bytes: return "B"; case Units.Count: return "count"; case Units.Packets: return "packets"; case Units.Frames: return "frames"; case Units.FramesPerSecond: return "fps"; case Units.SquareMagnitude: return "mag\u00B2"; default: throw new ArgumentOutOfRangeException(nameof(units), $"{units}"); } } } } #endregion #region UnityAssetGuidAttributeDrawer.cs namespace Fusion.Editor { using System; using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(UnityAssetGuidAttribute))] [FusionPropertyDrawerMeta(HasFoldout = false)] internal class UnityAssetGuidAttributeDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { string guid; position.width -= 40; if (property.propertyType == SerializedPropertyType.Generic) { guid = DrawMangledRawGuid(position, property, label); } else if (property.propertyType == SerializedPropertyType.String) { using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { EditorGUI.PropertyField(position, property, GUIContent.none, false); guid = property.stringValue; } } else { throw new InvalidOperationException(); } string assetPath = string.Empty; bool parsable = GUID.TryParse(guid, out _); if (parsable) { ClearError(); assetPath = AssetDatabase.GUIDToAssetPath(guid); } using (new FusionEditorGUI.EnabledScope(!string.IsNullOrEmpty(assetPath))) { position.x += position.width; position.width = 40; if (GUI.Button(position, "Ping")) { EditorGUIUtility.PingObject(AssetDatabase.LoadMainAssetAtPath(assetPath)); } } if (string.IsNullOrEmpty(assetPath)) { if (!parsable && !string.IsNullOrEmpty(guid)) { SetError($"Invalid GUID: {guid}"); } else if (!string.IsNullOrEmpty(guid)) { SetWarning($"GUID not found"); } } else { var asset = AssetDatabase.LoadMainAssetAtPath(assetPath); if (asset == null) { SetError($"Asset with this guid does not exist. Last path:\n{assetPath}"); } else { SetInfo($"Asset path:\n{assetPath}"); } } } private unsafe string DrawMangledRawGuid(Rect position, SerializedProperty property, GUIContent label) { var inner = property.Copy(); inner.Next(true); if (inner.depth != property.depth + 1 || !inner.isFixedBuffer || inner.fixedBufferSize != 2) { throw new InvalidOperationException(); } var prop0 = inner.GetFixedBufferElementAtIndex(0); var prop1 = inner.GetFixedBufferElementAtIndex(1); string guid; unsafe { var rawMangled = stackalloc long[2]; rawMangled[0] = prop0.longValue; rawMangled[1] = prop1.longValue; Guid guidStruct = default; CopyAndMangleGuid((byte*)rawMangled, (byte*)&guidStruct); using (new FusionEditorGUI.PropertyScope(position, label, property)) { EditorGUI.BeginChangeCheck(); guid = EditorGUI.TextField(position, label, guidStruct.ToString("N")); if (EditorGUI.EndChangeCheck()) { if (Guid.TryParse(guid, out guidStruct)) { CopyAndMangleGuid((byte*)&guidStruct, (byte*)rawMangled); prop0.longValue = rawMangled[0]; prop1.longValue = rawMangled[1]; } else { SetError($"Unable to parse {guid}"); } } } } return guid; } public static unsafe void CopyAndMangleGuid(byte* src, byte* dst) { dst[0] = src[3]; dst[1] = src[2]; dst[2] = src[1]; dst[3] = src[0]; dst[4] = src[5]; dst[5] = src[4]; dst[6] = src[7]; dst[7] = src[6]; dst[8] = src[8]; dst[9] = src[9]; dst[10] = src[10]; dst[11] = src[11]; dst[12] = src[12]; dst[13] = src[13]; dst[14] = src[14]; dst[15] = src[15]; } public bool HasFoldout(SerializedProperty property) { return false; } } } #endregion #region UnityResourcePathAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(UnityResourcePathAttribute))] internal class UnityResourcePathAttributeDrawer : PropertyDrawerWithErrorHandling { protected override void OnGUIInternal(Rect position, SerializedProperty property, GUIContent label) { var attrib = (UnityResourcePathAttribute)attribute; using (new FusionEditorGUI.PropertyScopeWithPrefixLabel(position, label, property, out position)) { position.width -= 40; EditorGUI.PropertyField(position, property, GUIContent.none, false); Object asset = null; var path = property.stringValue; if (string.IsNullOrEmpty(path)) { ClearError(); } else { asset = Resources.Load(path, attrib.ResourceType); if (asset == null) { SetError($"Resource of type {attrib.ResourceType} not found at {path}"); } else { SetInfo(AssetDatabase.GetAssetPath(asset)); } } using (new EditorGUI.DisabledScope(asset == null)) { position.x += position.width; position.width = 40; if (GUI.Button(position, "Ping")) // ping the main asset { EditorGUIUtility.PingObject(Resources.Load(path)); } } } } } } #endregion #region WarnIfAttributeDrawer.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; partial class WarnIfAttributeDrawer : MessageIfDrawerBase { private new WarnIfAttribute Attribute => (WarnIfAttribute)attribute; protected override bool IsBox => Attribute.AsBox; protected override string Message => Attribute.Message; protected override MessageType MessageType => MessageType.Warning; protected override Color InlineBoxColor => FusionEditorSkin.WarningInlineBoxColor; protected override Texture MessageIcon => FusionEditorSkin.WarningIcon; } [CustomPropertyDrawer(typeof(WarnIfAttribute))] [RedirectCustomPropertyDrawer(typeof(WarnIfAttribute), typeof(WarnIfAttributeDrawer))] partial class PropertyDrawerForArrayWorkaround { } } #endregion #region ArrayLengthAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Collections; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class ArrayLengthAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, ArrayLengthAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : Attribute { public ArrayLengthAttribute SourceAttribute; } class OdinDrawer : OdinAttributeDrawer { protected override bool CanDrawAttributeProperty(InspectorProperty property) { return property.GetUnityPropertyType() == SerializedPropertyType.ArraySize; } protected override void DrawPropertyLayout(GUIContent label) { var valEntry = Property.ValueEntry; var weakValues = valEntry.WeakValues; for (int i = 0; i < weakValues.Count; ++i) { var values = (IList)weakValues[i]; if (values == null) { continue; } var arraySize = values.Count; var attr = Attribute.SourceAttribute; if (arraySize < attr.MinLength) { arraySize = attr.MinLength; } else if (arraySize > attr.MaxLength) { arraySize = attr.MaxLength; } if (values.Count != arraySize) { if (values is Array array) { var newArr = Array.CreateInstance(array.GetType().GetElementType(), arraySize); Array.Copy(array, newArr, Math.Min(array.Length, arraySize)); weakValues.ForceSetValue(i, newArr); } else { while (values.Count > arraySize) { values.RemoveAt(values.Count - 1); } while (values.Count < arraySize) { values.Add(null); } } weakValues.ForceMarkDirty(); } } CallNextDrawer(label); } } } } #endif #endregion #region BinaryDataAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using Sirenix.OdinInspector; partial class BinaryDataAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, BinaryDataAttribute attribute) { return new[] { new DrawWithUnityAttribute() }; } } } #endif #endregion #region DoIfAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Linq; using System.Reflection; using Sirenix.OdinInspector.Editor; using UnityEngine; partial class DoIfAttributeDrawer { protected abstract class OdinProxyAttributeBase : Attribute { public DoIfAttributeBase SourceAttribute; } protected abstract class OdinDrawerBase : OdinAttributeDrawer where T : OdinProxyAttributeBase { protected override bool CanDrawAttributeProperty(InspectorProperty property) { if (property.IsArrayElement(out _)) { return false; } return true; } protected override void DrawPropertyLayout(GUIContent label) { var doIf = this.Attribute.SourceAttribute; bool allPassed = true; bool anyPassed = false; var targetProp = Property.FindPropertyRelativeToParent(doIf.ConditionMember); if (targetProp == null) { var objType = Property.ParentType; if (!_cachedGetters.TryGetValue((objType, doIf.ConditionMember), out var getter)) { // maybe this is a top-level property then and we can use reflection? if (Property.GetValueDepth() != 0) { if (doIf.ErrorOnConditionMemberNotFound) { FusionEditorLog.ErrorInspector($"Can't check condition for {Property.Path}: non-SerializedProperty checks only work for top-level properties"); } } else { try { _cachedGetters.Add((objType, doIf.ConditionMember), Property.ParentType.CreateGetter(doIf.ConditionMember, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy)); } catch (Exception e) { if (doIf.ErrorOnConditionMemberNotFound) { FusionEditorLog.ErrorInspector($"Can't check condition for {Property.Path}: unable to create getter for {doIf.ConditionMember} with exception {e}"); } } } } if (getter != null) { foreach (var obj in Property.GetValueParent().ValueEntry.WeakValues) { var value = getter(obj); if (DoIfAttributeDrawer.CheckCondition(doIf, value)) { anyPassed = true; } else { allPassed = false; } } } } else { foreach (var value in targetProp.ValueEntry.WeakValues) { if (DoIfAttributeDrawer.CheckCondition(doIf, value)) { anyPassed = true; } else { allPassed = false; } } } DrawPropertyLayout(label, allPassed, anyPassed); } protected abstract void DrawPropertyLayout(GUIContent label, bool allPassed, bool anyPassed); } } } #endif #endregion #region DrawIfAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class DrawIfAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, DrawIfAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : OdinProxyAttributeBase { } class OdinDrawer : OdinDrawerBase { protected override void DrawPropertyLayout(GUIContent label, bool allPassed, bool anyPassed) { var attribute = (DrawIfAttribute)Attribute.SourceAttribute; if (!allPassed) { if (attribute.Hide) { return; } } using (new EditorGUI.DisabledGroupScope(!allPassed)) { base.CallNextDrawer(label); } } } } } #endif #endregion #region DrawInlineAttributeDrawer.Odin.cs namespace Fusion.Editor { partial class DrawInlineAttributeDrawer { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, DrawInlineAttribute attribute) { return new System.Attribute[] { new Sirenix.OdinInspector.InlinePropertyAttribute(), new Sirenix.OdinInspector.HideLabelAttribute() }; } #endif } } #endregion #region ErrorIfAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class ErrorIfAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, ErrorIfAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : OdinProxyAttributeBase { } class OdinDrawer : OdinDrawerBase { protected override void DrawPropertyLayout(GUIContent label, bool allPassed, bool anyPassed) { var attribute = (ErrorIfAttribute)Attribute.SourceAttribute; base.CallNextDrawer(label); if (anyPassed) { using (new FusionEditorGUI.ErrorScope(attribute.Message)) { } } } } } } #endif #endregion #region FieldEditorButtonAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Linq; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class FieldEditorButtonAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, FieldEditorButtonAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : Attribute { public FieldEditorButtonAttribute SourceAttribute; } class OdinDrawer : OdinAttributeDrawer { protected override bool CanDrawAttributeProperty(InspectorProperty property) { return !property.IsArrayElement(out _); } protected override void DrawPropertyLayout(GUIContent label) { CallNextDrawer(label); var buttonRect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()); var attribute = Attribute.SourceAttribute; var root = this.Property.SerializationRoot; var targetType = root.ValueEntry.TypeOfValue; var targetObjects = root.ValueEntry.WeakValues .OfType() .ToArray(); if (DrawButton(buttonRect, attribute, targetType, targetObjects)) { this.Property.MarkSerializationRootDirty(); } } } } } #endif #endregion #region HideArrayElementLabelAttributeDrawer.Odin.cs namespace Fusion.Editor { partial class HideArrayElementLabelAttributeDrawer { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, HideArrayElementLabelAttribute attribute) { // not yet supported return System.Array.Empty(); } #endif } } #endregion #region InlineHelpAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Linq; using System.Reflection; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class InlineHelpAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, InlineHelpAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : Attribute { public InlineHelpAttribute SourceAttribute; } class OdinDrawer : OdinAttributeDrawer { protected override bool CanDrawAttributeProperty(InspectorProperty property) { if (property.IsArrayElement(out _)) { return false; } var helpContent = GetHelpContent(property, true); if (helpContent == GUIContent.none) { return false; } return true; } private Rect _lastRect; private bool GetHasFoldout() { var (meta, _) = Property.GetNextPropertyDrawerMetaAttribute(Attribute); if (meta != null) { return meta.HasFoldout; } return Property.GetUnityPropertyType() == SerializedPropertyType.Generic; } protected override void DrawPropertyLayout(GUIContent label) { Rect buttonRect = default; bool wasExpanded = false; bool hasFoldout = GetHasFoldout(); Rect propertyRect = _lastRect; var helpContent = GetHelpContent(Property, Attribute.SourceAttribute.ShowTypeHelp); using (new FusionEditorGUI.GUIContentScope(label)) { (wasExpanded, buttonRect) = InlineHelpAttributeDrawer.DrawInlineHelpBeforeProperty(label, helpContent, _lastRect, Property.Path, EditorGUI.indentLevel, hasFoldout, Property.SerializationRoot); EditorGUILayout.BeginVertical(); this.CallNextDrawer(label); EditorGUILayout.EndVertical(); } if (Event.current.type == EventType.Repaint) { _lastRect = GUILayoutUtility.GetLastRect(); } if (propertyRect.width > 1 && propertyRect.height > 1) { if (wasExpanded) { var height = FusionEditorGUI.GetInlineBoxSize(helpContent).y; EditorGUILayout.GetControlRect(false, height); propertyRect.height += FusionEditorGUI.GetInlineBoxSize(helpContent).y; } DrawInlineHelpAfterProperty(buttonRect, wasExpanded, helpContent, propertyRect); } } private GUIContent GetHelpContent(InspectorProperty property, bool includeTypeHelp) { var parentType = property.ValueEntry.ParentType; var memberInfo = parentType.GetField(property.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); return FusionCodeDoc.FindEntry(memberInfo, includeTypeHelp) ?? GUIContent.none; } } } } #endif #endregion #region LayerMatrixAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Collections; using System.Linq; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEditor.Animations; using UnityEngine; partial class LayerMatrixAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, LayerMatrixAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : Attribute { public LayerMatrixAttribute SourceAttribute; } class OdinDrawer : OdinAttributeDrawer { protected override void DrawPropertyLayout(GUIContent label) { var rect = EditorGUILayout.GetControlRect(); var valueRect = EditorGUI.PrefixLabel(rect, label); if (GUI.Button(valueRect, "Edit")) { int[] values = (int[])this.Property.ValueEntry.WeakValues[0]; PopupWindow.Show(valueRect, new LayerMatrixPopup(label.text, (layerA, layerB) => { if (layerA >= values.Length) { return false; } return (values[layerA] & (1 << layerB)) != 0; }, (layerA, layerB, val) => { if (Mathf.Max(layerA, layerB) >= values.Length) { Array.Resize(ref values, Mathf.Max(layerA, layerB) + 1); } if (val) { values[layerA] |= (1 << layerB); values[layerB] |= (1 << layerA); } else { values[layerA] &= ~(1 << layerB); values[layerB] &= ~(1 << layerA); } // sync other values for (int i = 1; i < this.Property.ValueEntry.ValueCount; ++i) { this.Property.ValueEntry.WeakValues.ForceSetValue(i, values.Clone()); } Property.MarkSerializationRootDirty(); })); } } } } } #endif #endregion #region FusionOdinAttributeConverterAttribute.cs namespace Fusion.Editor { using System; [AttributeUsage(AttributeTargets.Method)] public class FusionOdinAttributeConverterAttribute : Attribute { } } #endregion #region FusionOdinAttributeProcessor.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Sirenix.Utilities; using UnityEditor; using UnityEngine; public partial class FusionOdinAttributeProcessor : Sirenix.OdinInspector.Editor.OdinAttributeProcessor { public override void ProcessChildMemberAttributes(Sirenix.OdinInspector.Editor.InspectorProperty parentProperty, MemberInfo member, List attributes) { for (int i = 0; i < attributes.Count; ++i) { var attribute = attributes[i]; if (attribute is PropertyAttribute) { var drawerType = FusionEditorGUI.GetDrawerTypeIncludingWorkarounds(attribute); if (drawerType != null) { var method = drawerType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .FirstOrDefault(x => x.IsDefined(typeof(FusionOdinAttributeConverterAttribute))); if (method != null) { var replacementAttributes = (System.Attribute[])method.Invoke(null, new object[] { member, attribute }) ?? Array.Empty(); attributes.RemoveAt(i); FusionEditorLog.TraceInspector($"Replacing attribute {attribute.GetType().FullName} of {member.ToString()} with {string.Join(", ", replacementAttributes.Select(x => x.GetType().FullName))}"); if (replacementAttributes.Length > 0) { attributes.InsertRange(i, replacementAttributes); } i += replacementAttributes.Length - 1; continue; } } if (attribute is DecoratingPropertyAttribute) { FusionEditorLog.Warn($"Unable to replace {nameof(DecoratingPropertyAttribute)}-derived attribute: {attribute.GetType().FullName}"); attributes.RemoveAt(i--); } } } } } } #endif #endregion #region FusionOdinExtensions.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Reflection; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; public static class FusionOdinExtensions { public static bool IsArrayElement(this InspectorProperty property, out int index) { var propertyPath = property.UnityPropertyPath; if (!propertyPath.EndsWith("]", StringComparison.Ordinal)) { index = -1; return false; } var indexStart = propertyPath.LastIndexOf("[", StringComparison.Ordinal); if (indexStart < 0) { index = -1; return false; } index = int.Parse(propertyPath.Substring(indexStart + 1, propertyPath.Length - indexStart - 2)); return true; } public static bool IsArrayProperty(this InspectorProperty property) { var memberType = property.Info.TypeOfValue; if (!memberType.IsArrayOrList()) { return false; } return true; } public static int GetValueDepth(this InspectorProperty property) { int depth = 0; var parent = property.GetValueParent(); while (parent?.IsTreeRoot == false) { ++depth; parent = parent.GetValueParent(); } return depth; } public static InspectorProperty GetValueParent(this InspectorProperty property) { var parent = property.Parent; while (parent?.Info.PropertyType == PropertyType.Group) { parent = parent.Parent; } return parent; } public static SerializedPropertyType GetUnityPropertyType(this InspectorProperty inspectorProperty) { if (inspectorProperty == null) { throw new ArgumentNullException(nameof(inspectorProperty)); } var valueType = inspectorProperty.ValueEntry.TypeOfValue; if (valueType == typeof(bool)) { return SerializedPropertyType.Boolean; } else if (valueType == typeof(int) || valueType == typeof(long) || valueType == typeof(short) || valueType == typeof(byte) || valueType == typeof(uint) || valueType == typeof(ulong) || valueType == typeof(ushort) || valueType == typeof(sbyte)) { return SerializedPropertyType.Integer; } else if (valueType == typeof(float) || valueType == typeof(double)) { return SerializedPropertyType.Float; } else if (valueType == typeof(string)) { return SerializedPropertyType.String; } else if (valueType == typeof(Color)) { return SerializedPropertyType.Color; } else if (valueType == typeof(LayerMask)) { return SerializedPropertyType.LayerMask; } else if (valueType == typeof(Vector2)) { return SerializedPropertyType.Vector2; } else if (valueType == typeof(Vector3)) { return SerializedPropertyType.Vector3; } else if (valueType == typeof(Vector4)) { return SerializedPropertyType.Vector4; } else if (valueType == typeof(Vector2Int)) { return SerializedPropertyType.Vector2Int; } else if (valueType == typeof(Vector3Int)) { return SerializedPropertyType.Vector3Int; } else if (valueType == typeof(Rect)) { return SerializedPropertyType.Rect; } else if (valueType == typeof(RectInt)) { return SerializedPropertyType.RectInt; } else if (valueType == typeof(AnimationCurve)) { return SerializedPropertyType.AnimationCurve; } else if (valueType == typeof(Bounds)) { return SerializedPropertyType.Bounds; } else if (valueType == typeof(BoundsInt)) { return SerializedPropertyType.BoundsInt; } else if (valueType == typeof(Gradient)) { return SerializedPropertyType.Gradient; } else if (valueType == typeof(Quaternion)) { return SerializedPropertyType.Quaternion; } else if (valueType.IsEnum) { return SerializedPropertyType.Enum; } else if (typeof(UnityEngine.Object).IsAssignableFrom(valueType)) { return SerializedPropertyType.ObjectReference; } else if (valueType.IsArrayOrList()) { return SerializedPropertyType.ArraySize; } return SerializedPropertyType.Generic; } public static InspectorProperty FindPropertyRelativeToParent(this InspectorProperty property, string path) { InspectorProperty referenceProperty = property; int parentIndex = 0; do { if (referenceProperty.GetValueParent() == null) { return null; } referenceProperty = referenceProperty.GetValueParent(); } while (path[parentIndex++] == '^'); if (parentIndex > 1) { path = path.Substring(parentIndex - 1); } var parts = path.Split('.'); if (parts.Length == 0) { return null; } foreach (var part in parts) { var child = referenceProperty.Children[part]; if (child != null) { referenceProperty = child; } else { return null; } } return referenceProperty; } public static (FusionPropertyDrawerMetaAttribute, Attribute) GetNextPropertyDrawerMetaAttribute(this InspectorProperty property, Attribute referenceAttribute) { var attributeIndex = referenceAttribute == null ? -1 : property.Attributes.IndexOf(referenceAttribute); for (int i = attributeIndex + 1; i < property.Attributes.Count; ++i) { var otherAttribute = property.Attributes[i]; if (otherAttribute is DrawerPropertyAttribute == false) { continue; } var attributeDrawerType = FusionEditorGUI.GetDrawerTypeIncludingWorkarounds(otherAttribute); if (attributeDrawerType == null) { continue; } var meta = attributeDrawerType.GetCustomAttribute(); if (meta != null) { return (meta, otherAttribute); } } var propertyDrawerType = UnityInternal.ScriptAttributeUtility.GetDrawerTypeForType(property.ValueEntry.TypeOfValue, null, false); if (propertyDrawerType != null) { var meta = propertyDrawerType.GetCustomAttribute(); if (meta != null) { return (meta, null); } } return (null, null); } } } #endif #endregion #region ReadOnlyAttributeDrawer.Odin.cs namespace Fusion.Editor { partial class ReadOnlyAttributeDrawer { #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, ReadOnlyAttribute attribute) { if (attribute.InEditMode && attribute.InPlayMode) { return new[] { new Sirenix.OdinInspector.ReadOnlyAttribute() }; } if (attribute.InEditMode) { return new[] { new Sirenix.OdinInspector.DisableInEditorModeAttribute() }; } if (attribute.InPlayMode) { return new[] { new Sirenix.OdinInspector.DisableInPlayModeAttribute() }; } return System.Array.Empty(); } #endif } } #endregion #region SerializeReferenceTypePickerDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; partial class SerializeReferenceTypePickerAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, SerializeReferenceTypePickerAttribute attribute) { return Array.Empty(); } } } #endif #endregion #region UnitAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using System.Reflection; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class UnitAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, UnitAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : Attribute { public UnitAttribute SourceAttribute; } class OdinUnitAttributeDrawer : Sirenix.OdinInspector.Editor.OdinAttributeDrawer { private GUIContent _label; private Rect _lastRect; protected override bool CanDrawAttributeProperty(Sirenix.OdinInspector.Editor.InspectorProperty property) { for (Attribute attrib = null;;) { var (meta, nextAttribute) = property.GetNextPropertyDrawerMetaAttribute(attrib); attrib = nextAttribute; if (meta?.HandlesUnits == true) { if (attrib is OdinAttributeProxy == false) { return false; } } if (meta == null || attrib == null) { break; } } switch (property.GetUnityPropertyType()) { case SerializedPropertyType.ArraySize: return false; default: return true; } } protected sealed override void DrawPropertyLayout(GUIContent label) { using (new EditorGUILayout.VerticalScope()) { this.CallNextDrawer(label); } if (Event.current.type == EventType.Repaint) { _lastRect = GUILayoutUtility.GetLastRect(); } if (_lastRect.width > 1 && _lastRect.height > 1) { _label ??= new GUIContent(); _label.text = UnitToLabel(this.Attribute.SourceAttribute.Unit); DrawUnitOverlay(_lastRect, _label, Property.GetUnityPropertyType(), false, odinStyle: true); } } } } } #endif #endregion #region WarnIfAttributeDrawer.Odin.cs #if ODIN_INSPECTOR && !FUSION_ODIN_DISABLED namespace Fusion.Editor { using System; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; partial class WarnIfAttributeDrawer { [FusionOdinAttributeConverter] static System.Attribute[] ConvertToOdinAttributes(System.Reflection.MemberInfo memberInfo, WarnIfAttribute attribute) { return new[] { new OdinAttributeProxy() { SourceAttribute = attribute } }; } class OdinAttributeProxy : OdinProxyAttributeBase { } class OdinDrawer : OdinDrawerBase { protected override void DrawPropertyLayout(GUIContent label, bool allPassed, bool anyPassed) { var attribute = (WarnIfAttribute)Attribute.SourceAttribute; base.CallNextDrawer(label); if (anyPassed) { using (new FusionEditorGUI.WarningScope(attribute.Message)) { } } } } } } #endif #endregion #region INetworkAssetSourceFactory.cs namespace Fusion.Editor { using UnityEditor; public partial interface INetworkAssetSourceFactory { int Order { get; } } public readonly partial struct NetworkAssetSourceFactoryContext { public readonly int InstanceID; public readonly string AssetGuid; public readonly string AssetName; public readonly bool IsMainAsset; public string AssetPath => AssetDatabaseUtils.GetAssetPathOrThrow(InstanceID); public NetworkAssetSourceFactoryContext(string assetGuid, int instanceID, string assetName, bool isMainAsset) { AssetGuid = assetGuid; InstanceID = instanceID; AssetName = assetName; IsMainAsset = isMainAsset; } public NetworkAssetSourceFactoryContext(HierarchyProperty hierarchyProperty) { AssetGuid = hierarchyProperty.guid; InstanceID = hierarchyProperty.instanceID; AssetName = hierarchyProperty.name; IsMainAsset = hierarchyProperty.isMainRepresentation; } public NetworkAssetSourceFactoryContext(UnityEngine.Object obj) { if (!obj) { throw new System.ArgumentNullException(nameof(obj)); } var instanceId = obj.GetInstanceID(); (AssetGuid, _) = AssetDatabaseUtils.GetGUIDAndLocalFileIdentifierOrThrow(instanceId); InstanceID = instanceId; AssetName = obj.name; IsMainAsset = AssetDatabase.IsMainAsset(instanceId); } } } #endregion #region NetworkAssetSourceFactoryAddressable.cs #if (FUSION_ADDRESSABLES || FUSION_ENABLE_ADDRESSABLES) && !FUSION_DISABLE_ADDRESSABLES namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; public partial class NetworkAssetSourceFactoryAddressable : INetworkAssetSourceFactory { public const int Order = 800; int INetworkAssetSourceFactory.Order => Order; protected bool TryCreateInternal(in NetworkAssetSourceFactoryContext context, out TSource result) where TSource : NetworkAssetSourceAddressable, new() where TAsset : UnityEngine.Object { var addressableEntry = _guidToParentAddressable.Value[context.AssetGuid].SingleOrDefault(); if (addressableEntry == null) { result = default; return false; } result = new TSource() { Address = new(addressableEntry.guid) { SubObjectName = context.IsMainAsset ? string.Empty : context.AssetName, }, }; return true; } readonly Lazy> _guidToParentAddressable = new(() => CreateAddressablesLookup()); static ILookup CreateAddressablesLookup() { var assetList = new List(128); var assetsSettings = AddressableAssetSettingsDefaultObject.Settings; if (assetsSettings == null) { throw new System.InvalidOperationException("Unable to load Addressables settings. This may be due to an outdated Addressables version."); } Func groupFilter = null; CreateAddressablesGroupFilter(ref groupFilter); Func entryFilter = null; CreateAddressablesEntryFilter(ref entryFilter); assetsSettings.GetAllAssets(assetList, true, groupFilter: groupFilter, entryFilter: entryFilter); return assetList.Where(x => !string.IsNullOrEmpty(x.guid)).ToLookup(x => x.guid); } static partial void CreateAddressablesEntryFilter(ref Func filter); static partial void CreateAddressablesGroupFilter(ref Func filter); } } #endif #endregion #region NetworkAssetSourceFactoryResource.cs namespace Fusion.Editor { public partial class NetworkAssetSourceFactoryResource : INetworkAssetSourceFactory { public const int Order = 1000; int INetworkAssetSourceFactory.Order => Order; protected bool TryCreateInternal(in NetworkAssetSourceFactoryContext context, out TSource result) where TSource : NetworkAssetSourceResource, new() where TAsset : UnityEngine.Object { if (!PathUtils.TryMakeRelativeToFolder(context.AssetPath, "/Resources/", out var resourcePath)) { result = default; return false; } var withoutExtension = PathUtils.GetPathWithoutExtension(resourcePath); result = new TSource() { ResourcePath = withoutExtension, SubObjectName = context.IsMainAsset ? string.Empty : context.AssetName, }; return true; } } } #endregion #region NetworkAssetSourceFactoryStatic.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; public partial class NetworkAssetSourceFactoryStatic : INetworkAssetSourceFactory { public const int Order = int.MaxValue; int INetworkAssetSourceFactory.Order => Order; protected bool TryCreateInternal(in NetworkAssetSourceFactoryContext context, out TSource result) where TSource : NetworkAssetSourceStaticLazy, new() where TAsset : UnityEngine.Object { if (typeof(TAsset).IsSubclassOf(typeof(Component))) { var prefab = (GameObject)EditorUtility.InstanceIDToObject(context.InstanceID); result = new TSource() { Object = prefab.GetComponent() }; } else { result = new TSource() { Object = new(context.InstanceID) }; } return true; } } } #endregion #endregion #region Assets/Photon/Fusion/Editor/FusionEditorConfigImporter.cs namespace Fusion.Editor { using System.IO; using UnityEditor.AssetImporters; using UnityEngine; [ScriptedImporter(0, "editorconfig")] public class FusionEditorConfigImporter : ScriptedImporter { public override void OnImportAsset(AssetImportContext ctx) { var path = ctx.assetPath; var contents = File.ReadAllText(path); // create internal text asset for convenience var mainAsset = new TextAsset(contents); ctx.AddObjectToAsset("main", mainAsset); ctx.SetMainObject(mainAsset); // write the actual editorconfig for editors to consume var editorConfigPath = Path.Combine(Path.GetDirectoryName(path), ".editorconfig"); File.WriteAllText(editorConfigPath, contents); } } } #endregion #region Assets/Photon/Fusion/Editor/FusionHierarchyWindowOverlay.cs namespace Fusion.Editor { using System; using Fusion.Analyzer; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; internal class FusionHierarchyWindowOverlay { [RuntimeInitializeOnLoadMethod] public static void Initialize() { UnityEditor.EditorApplication.hierarchyWindowItemOnGUI -= HierarchyWindowOverlay; UnityEditor.EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowOverlay; } [StaticField(StaticFieldResetMode.None)] private static Lazy s_hierarchyOverlayLabelStyle = new Lazy(() => { var result = new GUIStyle(UnityEditor.EditorStyles.miniButton); result.alignment = TextAnchor.MiddleCenter; result.fontSize = 9; result.padding = new RectOffset(4, 4, 0, 0); result.fixedHeight = 13f; return result; }); [StaticField(StaticFieldResetMode.None)] private static GUIContent s_multipleInstancesContent = EditorGUIUtility.IconContent("Warning", "multiple"); private static void HierarchyWindowOverlay(int instanceId, Rect position) { var obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId); if (obj != null) { return; } // find a scene for this id Scene scene = default; for (int i = 0; i < SceneManager.sceneCount; ++i) { var s = SceneManager.GetSceneAt(i); if (s.handle == instanceId) { scene = s; break; } } if (!scene.IsValid()) { return; } var instances = NetworkRunner.Instances; NetworkRunner matchingRunner = null; bool multipleRunners = false; for (int i = 0; i < instances.Count; ++i) { var runner = instances[i]; if (runner.SimulationUnityScene == scene) { if (matchingRunner == null) { matchingRunner = runner; } else { multipleRunners = true; break; } } } if (!matchingRunner) { return; } var rect = new Rect(position) { xMin = position.xMax - 56, xMax = position.xMax - 2, yMin = position.yMin + 1, }; { if (multipleRunners) { if (EditorGUI.DropdownButton(rect, s_multipleInstancesContent, FocusType.Passive, s_hierarchyOverlayLabelStyle.Value)) { var menu = new GenericMenu(); for (int i = 0; i < instances.Count; ++i) { var runner = instances[i]; var otherScene = runner.SimulationUnityScene; if (!otherScene.IsValid()) { continue; } if (otherScene.handle == instanceId) { menu.AddItem(MakeRunnerContent(runner), false, () => { EditorGUIUtility.PingObject(runner); Selection.activeObject = runner; }); } } menu.ShowAsContext(); } } else { var runner = matchingRunner; if (GUI.Button(rect, MakeRunnerContent(runner), s_hierarchyOverlayLabelStyle.Value)) { EditorGUIUtility.PingObject(runner); Selection.activeGameObject = runner.gameObject; } } } GUIContent MakeRunnerContent(NetworkRunner runner) { return new GUIContent($"{runner.Mode} {(runner.LocalPlayer.IsRealPlayer ? "P" + runner.LocalPlayer.PlayerId.ToString() : "")}"); } } } } #endregion #region Assets/Photon/Fusion/Editor/FusionHubWindowUtils.cs // ---------------------------------------------------------------------------- // // PhotonNetwork Framework for Unity - Copyright (C) 2021 Exit Games GmbH // // // MenuItems and in-Editor scripts for PhotonNetwork. // // developer@exitgames.com // ---------------------------------------------------------------------------- namespace Fusion.Editor { #if FUSION_WEAVER && UNITY_EDITOR using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Text.RegularExpressions; using Photon.Realtime; using UnityEditor; using UnityEngine; using UnityEngine.UI; public partial class FusionHubWindow { /// /// Section Definition. /// internal class Section { public string Title; public string Description; public Action DrawMethod; public Icon Icon; public Section(string title, string description, Action drawMethod, Icon icon) { Title = title; Description = description; DrawMethod = drawMethod; Icon = icon; } } public enum Icon { Setup, Documentation, Samples, Community, ProductLogo, PhotonCloud, FusionIcon, } private static class Constants { public const string UrlFusionDocsOnline = "https://doc.photonengine.com/fusion/v2/"; public const string UrlFusionIntro = "https://doc.photonengine.com/fusion/v2/getting-started/fusion-introduction"; public const string UrlFusionSDK = "https://doc.photonengine.com/fusion/v2/getting-started/sdk-download"; public const string UrlCloudDashboard = "https://id.photonengine.com/account/signin?email="; public const string UrlDashboardProfile = "https://dashboard.photonengine.com/Account/Profile"; public const string UrlDashboard = "https://dashboard.photonengine.com/"; public const string UrlSampleSection = "https://doc.photonengine.com/fusion/v2/current/samples/overview"; public const string UrlFusion100 = "https://doc.photonengine.com/fusion/v2/tutorials/shared-mode-basics/overview"; public const string UrlFusionLoop = "https://doc.photonengine.com/fusion/v2/current/samples/fusion-application-loop"; public const string UrlHelloFusion = "https://doc.photonengine.com/fusion/v2/current/hello-fusion/hello-fusion"; public const string UrlHelloFusionVr = "https://doc.photonengine.com/fusion/v2/current/hello-fusion/hello-fusion-vr"; public const string UrlTanks = "https://doc.photonengine.com/fusion/v2/current/samples/fusion-tanknarok"; public const string UrlKarts = "https://doc.photonengine.com/fusion/v2/current/samples/fusion-karts"; public const string UrlDragonHuntersVR = "https://doc.photonengine.com/fusion/v2/current/samples/fusion-dragonhunters-vr"; public const string UrlFusionDocApi = "https://doc-api.photonengine.com/en/fusion/v2/index.html"; public const string WindowTitle = "Photon Fusion 2 Hub"; public const string Support = "You can contact the Photon Team using one of the following links. You can also go to Photon Documentation in order to get started."; public const string DiscordText = "Create a Photon account and join the Discord."; public const string DiscordHeader = "Community"; public const string DocumentationText = "Open the documentation."; public const string DocumentationHeader = "Documentation"; public const string WelcomeText = "Thank you for installing Photon Fusion 2.\n\n" + "Once you have set up your Fusion 2 App Id, explore the sections on the left to get started. " + "More samples, tutorials, and documentation are being added regularly - so check back often."; public const string RealtimeAppidSetupInstructions = @"An Fusion App Id Version 2 is required for networking. To acquire an Fusion App Id: - Open the Photon Dashboard (Log-in as required). - Select an existing Fusion 2 App Id, or; - Create a new one (make sure to select SDK Version 2). - Copy the App Id and paste into the field below (or into the PhotonAppSettings.asset). "; public const string GettingStartedInstructions = @"Links to demos, tutorials, API references and other information can be found on the PhotonEngine.com website."; } public Texture2D SetupIcon; public Texture2D DocumentationIcon; public Texture2D SamplesIcon; public Texture2D CommunityIcon; public Texture2D ProductLogo; public Texture2D PhotonCloudIcon; public Texture2D FusionIcon; public Texture2D CorrectIcon; private Texture2D GetIcon(Icon icon) { switch (icon) { case Icon.Setup: return SetupIcon; case Icon.Documentation: return DocumentationIcon; case Icon.Samples: return SamplesIcon; case Icon.Community: return CommunityIcon; case Icon.ProductLogo: return ProductLogo; case Icon.PhotonCloud: return PhotonCloudIcon; case Icon.FusionIcon: return FusionIcon; default: return null; } } [NonSerialized] private Section[] _sections; private static string releaseHistoryHeader; private static List releaseHistoryTextAdded; private static List releaseHistoryTextChanged; private static List releaseHistoryTextFixed; private static List releaseHistoryTextRemoved; private static List releaseHistoryTextInternal; private static string fusionReleaseHistory; public GUISkin FusionHubSkin; private static GUIStyle _navbarHeaderGraphicStyle; private static GUIStyle textLabelStyle; private static GUIStyle headerLabelStyle; private static GUIStyle releaseNotesStyle; private static GUIStyle headerTextStyle; private static GUIStyle buttonActiveStyle; private bool InitContent() { if (_ready.HasValue && _ready.Value) { return _ready.Value; } // skip while being loaded if (FusionHubSkin == null) { return false; } _sections = new[] { new Section("Welcome", "Welcome to Photon Fusion 2", DrawWelcomeSection, Icon.Setup), new Section("Fusion 2 Setup", "Setup Photon Fusion 2", DrawSetupSection, Icon.PhotonCloud), new Section("Tutorials & Samples", "Fusion Tutorials and Samples", DrawSamplesSection, Icon.Samples), new Section("Documentation", "Photon Fusion Documentation", DrawDocumentationSection, Icon.Documentation), new Section("Fusion Release Notes", "Fusion Release Notes", DrawFusionReleaseSection, Icon.Documentation), new Section("Support", "Support and Community Links", DrawSupportSection, Icon.Community), }; Color commonTextColor = Color.white; var _guiSkin = FusionHubSkin; _navbarHeaderGraphicStyle = new GUIStyle(_guiSkin.button) { alignment = TextAnchor.MiddleCenter }; headerTextStyle = new GUIStyle(_guiSkin.label) { fontSize = 18, padding = new RectOffset(12, 8, 8, 8), fontStyle = FontStyle.Bold, normal = { textColor = commonTextColor } }; buttonActiveStyle = new GUIStyle(_guiSkin.button) { fontStyle = FontStyle.Bold, normal = { background = _guiSkin.button.active.background, textColor = Color.white } }; textLabelStyle = new GUIStyle(_guiSkin.label) { wordWrap = true, normal = { textColor = commonTextColor }, richText = true, }; headerLabelStyle = new GUIStyle(textLabelStyle) { fontSize = 15, }; releaseNotesStyle = new GUIStyle(textLabelStyle) { richText = true, }; return (_ready = true).Value; } private static Action OpenURL(string url, params object[] args) { return () => { if (args.Length > 0) { url = string.Format(url, args); } Application.OpenURL(url); }; } protected static bool IsAppIdValid() { if (PhotonAppSettings.TryGetGlobal(out var global) && Guid.TryParse(global.AppSettings.AppIdFusion, out var guid)) { return true; } return false; } static string titleVersionReformat, sectionReformat, header1Reformat, header2Reformat, header3Reformat, classReformat; void InitializeFormatters() { titleVersionReformat = "$1"; sectionReformat = "$1"; header1Reformat = "$1"; header2Reformat = "$1"; header3Reformat = "$1"; classReformat = "$1"; } /// /// Converts readme files into Unity RichText. /// private void PrepareReleaseHistoryText() { if (sectionReformat == null || sectionReformat == "") { InitializeFormatters(); } // Fusion { try { var filePath = BuildPath(Application.dataPath, "Photon", "Fusion", "release_history.txt"); var text = (TextAsset)AssetDatabase.LoadAssetAtPath(filePath, typeof(TextAsset)); var baseText = text.text; // # baseText = Regex.Replace(baseText, @"^# (.*)", titleVersionReformat); baseText = Regex.Replace(baseText, @"(?<=\n)# (.*)", header1Reformat); // ## baseText = Regex.Replace(baseText, @"(?<=\n)## (.*)", header2Reformat); // ### baseText = Regex.Replace(baseText, @"(?<=\n)### (.*)", header3Reformat); // **Changes** baseText = Regex.Replace(baseText, @"(?<=\n)\*\*(.*)\*\*", sectionReformat); // `Class` baseText = Regex.Replace(baseText, @"\`([^\`]*)\`", classReformat); fusionReleaseHistory = baseText; } catch { fusionReleaseHistory = "Unable to load Release History."; } } // Realtime { try { var filePath = BuildPath(Application.dataPath, "Photon", "PhotonRealtime", "Code", "changes-realtime.txt"); var text = (TextAsset)AssetDatabase.LoadAssetAtPath(filePath, typeof(TextAsset)); var baseText = text.text; var regexVersion = new Regex(@"Version (\d+\.?)*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var regexAdded = new Regex(@"\b(Added:)(.*)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var regexChanged = new Regex(@"\b(Changed:)(.*)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var regexUpdated = new Regex(@"\b(Updated:)(.*)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var regexFixed = new Regex(@"\b(Fixed:)(.*)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var regexRemoved = new Regex(@"\b(Removed:)(.*)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var regexInternal = new Regex(@"\b(Internal:)(.*)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline); var matches = regexVersion.Matches(baseText); if (matches.Count > 0) { var currentVersionMatch = matches[0]; var lastVersionMatch = currentVersionMatch.NextMatch(); if (currentVersionMatch.Success && lastVersionMatch.Success) { Func> itemProcessor = (match) => { List resultList = new List(); for (int index = 0; index < match.Count; index++) { resultList.Add(match[index].Groups[2].Value.Trim()); } return resultList; }; string mainText = baseText.Substring(currentVersionMatch.Index + currentVersionMatch.Length, lastVersionMatch.Index - lastVersionMatch.Length - 1).Trim(); releaseHistoryHeader = currentVersionMatch.Value.Trim(); releaseHistoryTextAdded = itemProcessor(regexAdded.Matches(mainText)); releaseHistoryTextChanged = itemProcessor(regexChanged.Matches(mainText)); releaseHistoryTextChanged.AddRange(itemProcessor(regexUpdated.Matches(mainText))); releaseHistoryTextFixed = itemProcessor(regexFixed.Matches(mainText)); releaseHistoryTextRemoved = itemProcessor(regexRemoved.Matches(mainText)); releaseHistoryTextInternal = itemProcessor(regexInternal.Matches(mainText)); } } } catch { releaseHistoryHeader = "\nPlease look the file changes-realtime.txt"; releaseHistoryTextAdded = new List(); releaseHistoryTextChanged = new List(); releaseHistoryTextFixed = new List(); releaseHistoryTextRemoved = new List(); releaseHistoryTextInternal = new List(); } } } public static bool Toggle(bool value) { var toggle = new GUIStyle("Toggle") { margin = new RectOffset(), padding = new RectOffset() }; return EditorGUILayout.Toggle(value, toggle, GUILayout.Width(15)); } private static string BuildPath(params string[] parts) { var basePath = ""; foreach (var path in parts) { basePath = Path.Combine(basePath, path); } return basePath.Replace(Application.dataPath, Path.GetFileName(Application.dataPath)); } } #endif } #endregion #region Assets/Photon/Fusion/Editor/FusionInstaller.cs namespace Fusion.Editor { #if !FUSION_DEV using System; using System.IO; using UnityEditor; using UnityEditor.Build; using UnityEditor.PackageManager; using UnityEngine; [InitializeOnLoad] class FusionInstaller { const string DEFINE_VERSION = "FUSION2"; const string DEFINE = "FUSION_WEAVER"; const string PACKAGE_TO_SEARCH = "nuget.mono-cecil"; const string PACKAGE_TO_INSTALL = "com.unity.nuget.mono-cecil@1.10.2"; const string PACKAGES_DIR = "Packages"; const string MANIFEST_FILE = "manifest.json"; static FusionInstaller() { #if UNITY_SERVER var defines = PlayerSettings.GetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.Server); #else var group = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget); var defines = PlayerSettings.GetScriptingDefineSymbols(NamedBuildTarget.FromBuildTargetGroup(group)); #endif // Check for Defines // change based on https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2249 if (defines.Contains(DEFINE) && defines.Contains(DEFINE_VERSION)) { return; } if (!PlayerSettings.runInBackground) { FusionEditorLog.LogInstaller($"Setting {nameof(PlayerSettings)}.{nameof(PlayerSettings.runInBackground)} to true"); PlayerSettings.runInBackground = true; } var manifest = Path.Combine(Path.GetDirectoryName(Application.dataPath) ?? string.Empty, PACKAGES_DIR, MANIFEST_FILE); if (File.ReadAllText(manifest).Contains(PACKAGE_TO_SEARCH)) { FusionEditorLog.LogInstaller($"Setting '{DEFINE}' & '{DEFINE_VERSION}' Define"); // append defines if (defines.Contains(DEFINE) == false) { defines = $"{defines};{DEFINE}"; } if (defines.Contains(DEFINE_VERSION) == false) { defines = $"{defines};{DEFINE_VERSION}"; } #if UNITY_SERVER PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.Server, defines); #else PlayerSettings.SetScriptingDefineSymbols(NamedBuildTarget.FromBuildTargetGroup(group), defines); #endif } else { FusionEditorLog.LogInstaller($"Installing '{PACKAGE_TO_INSTALL}' package"); Client.Add(PACKAGE_TO_INSTALL); } } } #endif } #endregion #region Assets/Photon/Fusion/Editor/FusionSceneSetupAssistants.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using System.Collections.Generic; public static class FusionSceneSetupAssistants { [MenuItem("Tools/Fusion/Scene/Setup Networking in the Scene", false, FusionAssistants.PRIORITY_LOW + 1)] [MenuItem("GameObject/Fusion/Scene/Setup Networking in the Scene", false, FusionAssistants.PRIORITY + 1)] public static void AddNetworkingToScene() { (FusionBootstrap nds, NetworkRunner nr) n = AddNetworkStartup(); n.nr.gameObject.EnsureComponentExists(); // Get scene and mark scene as dirty. DirtyAndSaveScene(n.nds.gameObject.scene); } public static (FusionBootstrap, NetworkRunner) AddNetworkStartup() { // Restrict to single AudioListener to disallow multiple active in shared instance mode (preventing log spam) HandleAudioListeners(); // Restrict lights to single active instances node to Lights HandleLights(); // Add NetworkDebugRunner if missing var nds = FusionAssistants.EnsureExistsInScene("Prototype Network Start"); NetworkRunner nr = nds.RunnerPrefab == null ? null : nds.RunnerPrefab.TryGetComponent(out var found) ? found : null; // Add NetworkRunner to scene if the DebugStart doesn't have one as a prefab set already. if (nr == null) { // Add NetworkRunner to scene if NetworkDebugStart doesn't have one set as a prefab already. nr = FusionAssistants.EnsureExistsInScene("Prototype Runner"); nds.RunnerPrefab = nr; // The runner go is also our fallback spawn point... so raise it into the air a bit nr.transform.position = new Vector3(0, 3, 0); } return (nds, nr); } [MenuItem("Tools/Fusion/Scene/Add Current Scene To Build Settings", false, FusionAssistants.PRIORITY_LOW)] [MenuItem("GameObject/Fusion/Scene/Add Current Scene To Build Settings", false, FusionAssistants.PRIORITY)] public static void AddCurrentSceneToSettings() { DirtyAndSaveScene(SceneManager.GetActiveScene()); } public static void DirtyAndSaveScene(Scene scene) { UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene); var scenename = scene.path; // Give chance to save - required in order to build out. If users cancel will only be able to run in the editor. if (scenename == "") { UnityEditor.SceneManagement.EditorSceneManager.SaveModifiedScenesIfUserWantsTo(new Scene[] { scene }); scenename = scene.path; } // Add scene to Build and Fusion settings if (scenename != "") { scene.AddSceneToBuildSettings(); } } [MenuItem("Tools/Fusion/Scene/Setup Multi-Peer AudioListener Handling", false, FusionAssistants.PRIORITY_LOW + 1)] [MenuItem("GameObject/Fusion/Scene/Setup Multi-Peer AudioListener Handling", false, FusionAssistants.PRIORITY + 1)] public static void HandleAudioListeners() { int count = 0; foreach (var listener in Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { count++; listener.EnsureComponentHasVisibilityNode(); } Debug.Log($"{count} {nameof(AudioListener)}(s) found and given a {nameof(RunnerVisibilityLink)} component."); } [MenuItem("Tools/Fusion/Scene/Setup Multi-Peer Lights Handling", false, FusionAssistants.PRIORITY_LOW + 1)] [MenuItem("GameObject/Fusion/Scene/Setup Multi-Peer Lights Handling", false, FusionAssistants.PRIORITY + 1)] public static void HandleLights() { int count = 0; foreach (var listener in Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { count++; listener.EnsureComponentHasVisibilityNode(); } Debug.Log($"{count} {nameof(Light)}(s) found and given a {nameof(RunnerVisibilityLink)} component."); } public static void AddSceneToBuildSettings(this Scene scene) { var buildScenes = EditorBuildSettings.scenes; bool isInBuildScenes = false; foreach (var bs in buildScenes) { if (bs.path == scene.path) { isInBuildScenes = true; break; } } if (isInBuildScenes == false) { var buildList = new List(); buildList.Add(new EditorBuildSettingsScene(scene.path, true)); buildList.AddRange(buildScenes); Debug.Log($"Added '{scene.path}' as first entry in Build Settings."); EditorBuildSettings.scenes = buildList.ToArray(); } } } } #endregion #region Assets/Photon/Fusion/Editor/FusionUnitySurrogateBaseWrapper.cs namespace Fusion.Editor { using System; using Internal; using UnityEditor; using UnityEngine; internal class FusionUnitySurrogateBaseWrapper : ScriptableObject { [SerializeReference] public UnitySurrogateBase Surrogate; [NonSerialized] public SerializedProperty SurrogateProperty; [NonSerialized] public Type SurrogateType; } } #endregion #region Assets/Photon/Fusion/Editor/FusionWeaverTriggerImporter.cs namespace Fusion.Editor { using System.IO; using System.Linq; using UnityEditor; using UnityEditor.AssetImporters; using UnityEngine; [ScriptedImporter(1, ExtensionWithoutDot, NetworkProjectConfigImporter.ImportQueueOffset + 1)] public class FusionWeaverTriggerImporter : ScriptedImporter { public const string DependencyName = "FusionILWeaverTriggerImporter/ConfigHash"; public const string Extension = "." + ExtensionWithoutDot; public const string ExtensionWithoutDot = "fusionweavertrigger"; public override void OnImportAsset(AssetImportContext ctx) { ctx.DependsOnCustomDependency(DependencyName); ILWeaverUtils.RunWeaver(); } private static void RefreshDependencyHash() { if (EditorApplication.isCompiling || EditorApplication.isUpdating) { return; } var configPath = NetworkProjectConfigUtilities.GetGlobalConfigPath(); if (string.IsNullOrEmpty(configPath)) { return; } try { var cfg = NetworkProjectConfigImporter.LoadConfigFromFile(configPath); var hash = new Hash128(); foreach (var path in cfg.AssembliesToWeave) { hash.Append(path); } hash.Append(cfg.UseSerializableDictionary ? 1 : 0); hash.Append(cfg.NullChecksForNetworkedProperties ? 1 : 0); hash.Append(cfg.CheckRpcAttributeUsage ? 1 : 0); hash.Append(cfg.CheckNetworkedPropertiesBeingEmpty ? 1 : 0); AssetDatabase.RegisterCustomDependency(DependencyName, hash); AssetDatabase.Refresh(); } catch { // ignore the error } } private class Postprocessor : AssetPostprocessor { private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (var path in importedAssets) { if (path.EndsWith(NetworkProjectConfigImporter.Extension)) { EditorApplication.delayCall -= RefreshDependencyHash; EditorApplication.delayCall += RefreshDependencyHash; } } } } } } #endregion #region Assets/Photon/Fusion/Editor/ILWeaverUtils.cs namespace Fusion.Editor { using UnityEditor; using UnityEditor.Compilation; [InitializeOnLoad] public static class ILWeaverUtils { [MenuItem("Tools/Fusion/Run Weaver")] public static void RunWeaver() { CompilationPipeline.RequestScriptCompilation( #if UNITY_2021_1_OR_NEWER RequestScriptCompilationOptions.CleanBuildCache #endif ); } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkBehaviourEditor.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; [CustomEditor(typeof(NetworkBehaviour), true)] [CanEditMultipleObjects] public class NetworkBehaviourEditor : BehaviourEditor { internal const string NETOBJ_REQUIRED_WARN_TEXT = "This " + nameof(NetworkBehaviour) + " requires a " + nameof(NetworkObject) + " component to function."; IEnumerable ValidTargets => targets .Cast() .Where(x => x.Object && x.Object.IsValid && x.Object.gameObject.activeInHierarchy); [NonSerialized] int[] _buffer = Array.Empty(); public override void OnInspectorGUI() { base.PrepareOnInspectorGUI(); bool hasBeenApplied = false; #if !FUSION_DISABLE_NBEDITOR_PRESERVE_BACKING_FIELDS // serialize unchanged serialized state into zero-initialized memory; // this makes sure defaults are preserved TransferBackingFields(backingFieldsToState: true); #endif try { // after the original values have been saved, they can be overwritten with // whatever is in the state foreach (var target in ValidTargets) { target.CopyStateToBackingFields(); } // move C# fields to SerializedObject serializedObject.UpdateIfRequiredOrScript(); EditorGUI.BeginChangeCheck(); base.DrawDefaultInspector(); if (EditorGUI.EndChangeCheck()) { // serialized properties -> C# fields serializedObject.ApplyModifiedProperties(); hasBeenApplied = true; // C# fields -> state foreach (var target in ValidTargets) { if (target.Object.HasStateAuthority) { target.CopyBackingFieldsToState(false); } } } } finally { #if !FUSION_DISABLE_NBEDITOR_PRESERVE_BACKING_FIELDS // now restore the default values TransferBackingFields(backingFieldsToState: false); serializedObject.Update(); if (hasBeenApplied) { serializedObject.ApplyModifiedProperties(); } } #endif DrawNetworkObjectCheck(); DrawEditorButtons(); } unsafe bool TransferBackingFields(bool backingFieldsToState) { if (Allocator.REPLICATE_WORD_SIZE == sizeof(int)) { int offset = 0; bool hadChanges = false; int requiredSize = ValidTargets.Sum(x => x.WordCount); if (backingFieldsToState) { if (_buffer.Length >= requiredSize) { Array.Clear(_buffer, 0, _buffer.Length); } else { _buffer = new int[requiredSize]; } } else { if (_buffer.Length < requiredSize) { throw new InvalidOperationException("Buffer is too small"); } } fixed (int* p = _buffer) { foreach (var target in ValidTargets) { var ptr = target.Ptr; try { target.Ptr = p + offset; if (backingFieldsToState) { target.CopyBackingFieldsToState(false); } else { target.CopyStateToBackingFields(); } if (!hadChanges) { if (Native.MemCmp(target.Ptr, ptr, target.WordCount * Allocator.REPLICATE_WORD_SIZE) != 0) { hadChanges = true; } } } finally { target.Ptr = ptr; } offset += target.WordCount; } } return hadChanges; } } /// /// Checks if GameObject or parent GameObject has a NetworkObject, and draws a warning and buttons for adding one if not. /// /// void DrawNetworkObjectCheck() { var targetsWithoutNetworkObjects = targets.Cast().Where(x => x.transform.GetParentComponent() == false).ToList(); if (targetsWithoutNetworkObjects.Any()) { using (new FusionEditorGUI.WarningScope(NETOBJ_REQUIRED_WARN_TEXT, 6f)) { IEnumerable gameObjects = null; if (GUI.Button(EditorGUILayout.GetControlRect(false, 22), "Add Network Object")) { gameObjects = targetsWithoutNetworkObjects.Select(x => x.gameObject).Distinct(); } if (GUI.Button(EditorGUILayout.GetControlRect(false, 22), "Add Network Object to Root")) { gameObjects = targetsWithoutNetworkObjects.Select(x => x.transform.root.gameObject).Distinct(); } if (gameObjects != null) { foreach (var go in gameObjects) { Undo.AddComponent(go); } } } } } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkMecanimAnimatorEditor.cs namespace Fusion.Editor { using UnityEditor; [CustomEditor(typeof(NetworkMecanimAnimator))] public class NetworkMecanimAnimatorEditor : BehaviourEditor { public override void OnInspectorGUI() { var na = target as NetworkMecanimAnimator; if (na != null) { AnimatorControllerTools.GetHashesAndNames(na, null, null, ref na.TriggerHashes, ref na.StateHashes); na.TotalWords = na.GetWordCount().words; } base.OnInspectorGUI(); } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkObjectBakerEditTime.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; public class NetworkObjectBakerEditTime : NetworkObjectBaker { private Dictionary _executionOrderCache = new Dictionary(); protected override bool TryGetExecutionOrder(MonoBehaviour obj, out int order) { // is there a cached value? if (_executionOrderCache.TryGetValue(obj.GetType(), out var orderNullable)) { order = orderNullable ?? default; return orderNullable != null; } var monoScript = UnityEditor.MonoScript.FromMonoBehaviour(obj); if (monoScript) { orderNullable = UnityEditor.MonoImporter.GetExecutionOrder(monoScript); } else { orderNullable = null; } _executionOrderCache.Add(obj.GetType(), orderNullable); order = orderNullable ?? default; return orderNullable != null; } protected override void SetDirty(MonoBehaviour obj) { EditorUtility.SetDirty(obj); } protected override uint GetSortKey(NetworkObject obj) { var globalId = GlobalObjectId.GetGlobalObjectIdSlow(obj); int hash = 0; hash = HashCodeUtilities.GetHashCodeDeterministic(globalId.identifierType, hash); hash = HashCodeUtilities.GetHashCodeDeterministic(globalId.assetGUID, hash); hash = HashCodeUtilities.GetHashCodeDeterministic(globalId.targetObjectId, hash); hash = HashCodeUtilities.GetHashCodeDeterministic(globalId.targetPrefabId, hash); return (uint)hash; } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkObjectEditor.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else using UnityEditor.Experimental.SceneManagement; #endif [CustomEditor(typeof(NetworkObject), true)] [InitializeOnLoad] [CanEditMultipleObjects] public unsafe class NetworkObjectEditor : BehaviourEditor { private bool _runtimeInfoFoldout; private static PropertyInfo _isSpawnable = typeof(NetworkObject).GetPropertyOrThrow(nameof(NetworkObject.IsSpawnable)); private static FieldInfo _networkTypeId = typeof(NetworkObject).GetFieldOrThrow(nameof(NetworkObject.NetworkTypeId)); private static PropertyInfo _networkId = typeof(NetworkObject).GetPropertyOrThrow(nameof(NetworkObject.Id)); private static FieldInfo _nestingRoot = typeof(NetworkObjectHeader).GetFieldOrThrow(nameof(NetworkObjectHeader.NestingRoot)); private static FieldInfo _nestingKey = typeof(NetworkObjectHeader).GetFieldOrThrow(nameof(NetworkObjectHeader.NestingKey)); private static PropertyInfo _InputAuthority = typeof(NetworkObject).GetPropertyOrThrow(nameof(NetworkObject.InputAuthority)); private static PropertyInfo _StateAuthority = typeof(NetworkObject).GetPropertyOrThrow(nameof(NetworkObject.StateAuthority)); private static PropertyInfo _HasInputAuthority = typeof(NetworkObject).GetPropertyOrThrow(nameof(NetworkObject.HasInputAuthority)); private static PropertyInfo _HasStateAuthority = typeof(NetworkObject).GetPropertyOrThrow(nameof(NetworkObject.HasStateAuthority)); static string GetLoadInfoString(NetworkObjectGuid guid) { if (NetworkProjectConfigUtilities.TryGetGlobalPrefabSource(guid, out INetworkPrefabSource prefabSource)) { return prefabSource.Description; } return "Null"; } public override void OnInspectorGUI() { FusionEditorGUI.InjectScriptHeaderDrawer(serializedObject); FusionEditorGUI.ScriptPropertyField(serializedObject); // these properties' isExpanded are going to be used for foldouts; that's the easiest // way to get quasi-persistent foldouts var flagsProperty = serializedObject.FindPropertyOrThrow(nameof(NetworkObject.Flags)); var obj = (NetworkObject)base.target; var netObjType = typeof(NetworkObject); if (targets.Length == 1) { if (AssetDatabase.IsMainAsset(obj.gameObject) || PrefabStageUtility.GetPrefabStage(obj.gameObject)?.prefabContentsRoot == obj.gameObject) { Debug.Assert(!AssetDatabaseUtils.IsSceneObject(obj.gameObject)); if (!obj.Flags.IsVersionCurrent()) { using (new FusionEditorGUI.WarningScope("Prefab needs to be re-imported.")) { if (GUILayout.Button("Reimport")) { string assetPath = PrefabStageUtility.GetPrefabStage(obj.gameObject)?.assetPath ?? AssetDatabase.GetAssetPath(obj.gameObject); Debug.Assert(!string.IsNullOrEmpty(assetPath)); AssetDatabase.ImportAsset(assetPath); } } } else { EditorGUILayout.Space(); EditorGUILayout.LabelField("Prefab Settings", EditorStyles.boldLabel); // Is Spawnable { EditorGUI.BeginChangeCheck(); bool spawnable = EditorGUI.Toggle(FusionEditorGUI.LayoutHelpPrefix(this, _isSpawnable), _isSpawnable.Name, !obj.Flags.IsIgnored()); if (EditorGUI.EndChangeCheck()) { var value = obj.Flags.SetIgnored(!spawnable); serializedObject.FindProperty(nameof(NetworkObject.Flags)).intValue = (int)value; serializedObject.ApplyModifiedProperties(); } string loadInfo = "---"; if (spawnable) { string assetPath = PrefabStageUtility.GetPrefabStage(obj.gameObject)?.assetPath ?? AssetDatabase.GetAssetPath(obj.gameObject); if (!string.IsNullOrEmpty(assetPath)) { var guid = AssetDatabase.AssetPathToGUID(assetPath); loadInfo = GetLoadInfoString(NetworkObjectGuid.Parse(guid)); } } EditorGUILayout.LabelField("Prefab Source", loadInfo); } } } else if (AssetDatabaseUtils.IsSceneObject(obj.gameObject)) { if (!obj.Flags.IsVersionCurrent()) { if (!EditorApplication.isPlaying) { using (new FusionEditorGUI.WarningScope("This object hasn't been baked yet. Save the scene or enter playmode.")) { } } } } } if (EditorApplication.isPlaying && targets.Length == 1) { EditorGUILayout.Space(); flagsProperty.isExpanded = EditorGUILayout.Foldout(flagsProperty.isExpanded, "Runtime Info"); if (flagsProperty.isExpanded) { using (new FusionEditorGUI.BoxScope(null, 1)) { EditorGUI.LabelField(FusionEditorGUI.LayoutHelpPrefix(this, _networkTypeId), _networkTypeId.Name, obj.NetworkTypeId.ToString()); EditorGUILayout.Toggle("Is Valid", obj.IsValid); if (obj.IsValid) { EditorGUI.LabelField(FusionEditorGUI.LayoutHelpPrefix(this, _networkId), _networkId.Name, obj.Id.ToString()); EditorGUILayout.IntField("Word Count", NetworkObject.GetWordCount(obj)); bool headerIsNull = obj.Header == null; EditorGUI.LabelField(FusionEditorGUI.LayoutHelpPrefix(this, _nestingRoot), _nestingRoot.Name, headerIsNull ? "---" : obj.Header->NestingRoot.ToString()); EditorGUI.LabelField(FusionEditorGUI.LayoutHelpPrefix(this, _nestingKey), _nestingKey.Name, headerIsNull ? "---" : obj.Header->NestingKey.ToString()); EditorGUI.LabelField(FusionEditorGUI.LayoutHelpPrefix(this, _InputAuthority), _InputAuthority.Name, obj.InputAuthority.ToString()); EditorGUI.LabelField(FusionEditorGUI.LayoutHelpPrefix(this, _StateAuthority), _StateAuthority.Name, obj.StateAuthority.ToString()); EditorGUI.Toggle(FusionEditorGUI.LayoutHelpPrefix(this, _HasInputAuthority), _InputAuthority.Name, obj.HasInputAuthority); EditorGUI.Toggle(FusionEditorGUI.LayoutHelpPrefix(this, _HasStateAuthority), _StateAuthority.Name, obj.HasStateAuthority); EditorGUILayout.Toggle("Is Simulated", obj.Runner.Simulation.IsSimulated(obj)); EditorGUILayout.Toggle("Is Local PlayerObject", ReferenceEquals(obj.Runner.GetPlayerObject(obj.Runner.LocalPlayer), obj)); EditorGUILayout.Toggle("Has Main TRSP", obj.Meta.HasMainTRSP); EditorGUILayout.LabelField("Runtime Flags", obj.RuntimeFlags.ToString()); EditorGUILayout.LabelField("Header Flags", obj.Header->Flags.ToString()); if (obj.Runner.IsClient) { EditorGUILayout.IntField("Last Received Tick", obj.LastReceiveTick); } } } } } EditorGUI.BeginChangeCheck(); var config = NetworkProjectConfig.Global; var isPlaying = EditorApplication.isPlaying; void DrawToggleFlag(NetworkObjectFlags flag, string name, bool? force = null) { var x = (obj.Flags & flag) == flag; var r = EditorGUILayout.Toggle(name, x); if (r != x || (force.HasValue && r != force.Value)) { if (force.HasValue) { r = force.Value; } if (r) { obj.Flags |= flag; } else { obj.Flags &= ~flag; } EditorUtility.SetDirty(obj); } } using (new EditorGUI.DisabledScope(isPlaying)) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Shared Mode Settings", EditorStyles.boldLabel); DrawToggleFlag(NetworkObjectFlags.MasterClientObject, "Is Master Client Object"); EditorGUI.BeginDisabledGroup((obj.Flags & NetworkObjectFlags.MasterClientObject) == NetworkObjectFlags.MasterClientObject); if ((obj.Flags & NetworkObjectFlags.MasterClientObject) == NetworkObjectFlags.MasterClientObject) { DrawToggleFlag(NetworkObjectFlags.AllowStateAuthorityOverride, "Allow State Authority Override", false); } else { DrawToggleFlag(NetworkObjectFlags.AllowStateAuthorityOverride, "Allow State Authority Override"); } EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup((obj.Flags & NetworkObjectFlags.AllowStateAuthorityOverride) == default); if ((obj.Flags & NetworkObjectFlags.MasterClientObject) == NetworkObjectFlags.MasterClientObject) { DrawToggleFlag(NetworkObjectFlags.DestroyWhenStateAuthorityLeaves, "Destroy When State Authority Leaves", false); } else { if ((obj.Flags & NetworkObjectFlags.AllowStateAuthorityOverride) == NetworkObjectFlags.AllowStateAuthorityOverride) { DrawToggleFlag(NetworkObjectFlags.DestroyWhenStateAuthorityLeaves, "Destroy When State Authority Leaves"); } else { DrawToggleFlag(NetworkObjectFlags.DestroyWhenStateAuthorityLeaves, "Destroy When State Authority Leaves", true); } } EditorGUI.EndDisabledGroup(); //var destroyWhenStateAuthLeaves = serializedObject.FindProperty(nameof(NetworkObject.DestroyWhenStateAuthorityLeaves)); //EditorGUILayout.PropertyField(destroyWhenStateAuthLeaves); // //var allowStateAuthorityOverride = serializedObject.FindProperty(nameof(NetworkObject.AllowStateAuthorityOverride)); //EditorGUILayout.PropertyField(allowStateAuthorityOverride); EditorGUILayout.Space(); EditorGUILayout.LabelField("Interest Management Settings", EditorStyles.boldLabel); var objectInterest = serializedObject.FindProperty(nameof(NetworkObject.ObjectInterest)); EditorGUILayout.PropertyField(objectInterest); if (objectInterest.intValue == (int)NetworkObject.ObjectInterestModes.AreaOfInterest) { //EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(NetworkObject.AreaOfInterestTransform))); } //using (new EditorGUI.IndentLevelScope()) { // EditorGUILayout.PropertyField(serializedObject.FindPropertyOrThrow(nameof(NetworkObject.DefaultInterestGroups))); //} } if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); } EditorGUILayout.Space(); EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); EditorGUILayout.Space(); EditorGUILayout.LabelField("Baked Data", EditorStyles.boldLabel); using (new FusionEditorGUI.BoxScope(null, 1)) { using (new EditorGUI.DisabledScope(true)) { using (new FusionEditorGUI.ShowMixedValueScope(flagsProperty.hasMultipleDifferentValues)) { FusionEditorGUI.LayoutSelectableLabel(EditorGUIUtility.TrTextContent(nameof(obj.Flags)), obj.Flags.ToString()); FusionEditorGUI.LayoutSelectableLabel(EditorGUIUtility.TrTextContent(nameof(obj.SortKey)), obj.SortKey.ToString("X8")); } using (new EditorGUI.IndentLevelScope()) { EditorGUILayout.PropertyField(serializedObject.FindPropertyOrThrow(nameof(NetworkObject.NestedObjects))); EditorGUILayout.PropertyField(serializedObject.FindPropertyOrThrow(nameof(NetworkObject.NetworkedBehaviours))); } } } // Runtime buttons if (obj.Runner && obj.Runner.IsRunning) { EditorGUILayout.Space(); EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); EditorGUILayout.Space(); // Input Authority Popup using (new EditorGUI.DisabledScope(obj.HasStateAuthority == false)) { var elements = GetInputAuthorityPopupContent(obj); var index = EditorGUILayout.Popup(_guiContentInputAuthority, elements.currentIndex, elements.content); if (index != elements.currentIndex) { obj.AssignInputAuthority(PlayerRef.FromIndex(elements.ids[index])); } } if (obj.Runner.GameMode == GameMode.Shared) { if (GUILayout.Button("Request State Authority")) { obj.RequestStateAuthority(); } } if (GUILayout.Button("Despawn")) { obj.Runner.Despawn(obj); } } } private static bool Set(UnityEngine.Object host, ref T field, T value, Action setDirty) { if (!EqualityComparer.Default.Equals(field, value)) { Trace($"Object dirty: {host} ({field} vs {value})"); setDirty?.Invoke(host); field = value; return true; } else { return false; } } private static bool Set(UnityEngine.Object host, ref T[] field, List value, Action setDirty) { var comparer = EqualityComparer.Default; if (field == null || field.Length != value.Count || !field.SequenceEqual(value, comparer)) { Trace($"Object dirty: {host} ({field} vs {value})"); setDirty?.Invoke(host); field = value.ToArray(); return true; } else { return false; } } [System.Diagnostics.Conditional("FUSION_EDITOR_TRACE")] private static void Trace(string msg) { Debug.Log($"[Fusion/NetworkObjectEditor] {msg}"); } public static NetworkObjectGuid GetPrefabGuid(NetworkObject prefab) { if (prefab == null) { throw new ArgumentNullException(nameof(prefab)); } if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(prefab, out var guidStr, out long _)) { throw new ArgumentException($"No guid for {prefab}", nameof(prefab)); } return NetworkObjectGuid.Parse(guidStr); } private static GUIContent[] _reusableContent; private static int[] _reusablePlayerIds; private static readonly GUIContent _guiContentEmpty = new GUIContent(""); private static readonly GUIContent _guiContentNone = new GUIContent("None"); private static readonly GUIContent _guiContentInputAuthority = new GUIContent("Input Authority"); private static (int[] ids, GUIContent[] content, int currentIndex) GetInputAuthorityPopupContent(NetworkObject obj) { int requiredLength = obj.Runner.ActivePlayers.Count() + 2; if (_reusableContent == null || requiredLength > _reusableContent.Length) { _reusablePlayerIds = new int[requiredLength]; _reusablePlayerIds[0] = -1; _reusablePlayerIds[1] = 0; _reusableContent = new GUIContent[requiredLength]; _reusableContent[0] = _guiContentNone; _reusableContent[1] = _guiContentEmpty; } int indexOfCurrentPlayer = 0; // clear for (int i = 2; i < _reusableContent.Length; i++) { _reusableContent[i] = _guiContentEmpty; } int index = 2; foreach (var player in obj.Runner.ActivePlayers) { _reusablePlayerIds[index] = player.PlayerId; _reusableContent[index] = new GUIContent($"Player {player.PlayerId}"); if (player.PlayerId == obj.InputAuthority.PlayerId) { indexOfCurrentPlayer = index; } index++; } return (_reusablePlayerIds, _reusableContent, indexOfCurrentPlayer); } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkObjectPostprocessor.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; public class NetworkObjectPostprocessor : AssetPostprocessor { public static event Action OnBakePrefab; public static event Action OnBakeScene; static NetworkObjectPostprocessor() { EditorSceneManager.sceneSaving += OnSceneSaving; EditorApplication.playModeStateChanged += OnPlaymodeChange; } static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { FusionEditorLog.TraceImport($"Postprocessing imported assets [{importedAssets.Length}]:\n{string.Join("\n", importedAssets)}"); bool rebuildPrefabHash = false; foreach (var path in importedAssets) { if (!IsPrefabPath(path)) { continue; } var go = AssetDatabase.LoadAssetAtPath(path); if (!go) { continue; } var isSpawnable = false; var needsBaking = false; var no = go.GetComponent(); if (no) { // NO prefab, needs labels adjusted and hash needs to be rebuilt rebuildPrefabHash = true; needsBaking = true; isSpawnable = !no.Flags.IsIgnored(); } if (AssetDatabaseUtils.SetLabel(go, NetworkProjectConfigImporter.FusionPrefabTag, isSpawnable)) { rebuildPrefabHash = true; AssetDatabase.ImportAsset(path); FusionEditorLog.TraceImport(path, "Labels were dirty"); } else if (no) { FusionEditorLog.TraceImport(path, "Labels up to date"); } if (needsBaking) { #if UNITY_2023_1_OR_NEWER || UNITY_2022_3_OR_NEWER if (Array.IndexOf(movedAssets, path) >= 0) { // attempting to bake a prefab that has been moved would hang the editor // https://issuetracker.unity3d.com/issues/editor-freezes-when-prefabutility-dot-loadprefabcontents-is-called-in-assetpostprocessor-dot-onpostprocessallassets-for-a-moved-prefab continue; } #endif FusionEditorLog.TraceImport(path, "Baking"); BakePrefab(path, out _); } } foreach (var path in movedAssets) { if (!IsPrefabPath(path)) { continue; } if (!AssetDatabaseUtils.HasLabel(path, NetworkProjectConfigImporter.FusionPrefabTag)) { continue; } rebuildPrefabHash = true; break; } foreach (var path in deletedAssets) { if (!IsPrefabPath(path)) { continue; } rebuildPrefabHash = true; break; } if (rebuildPrefabHash) { EditorApplication.delayCall -= NetworkProjectConfigImporter.RefreshNetworkObjectPrefabHash; EditorApplication.delayCall += NetworkProjectConfigImporter.RefreshNetworkObjectPrefabHash; } } static bool IsPrefabPath(string path) { return path.EndsWith(".prefab"); } static bool IsNetworkObjectPrefab(string path, out NetworkObject no) { if (!path.EndsWith(".prefab")) { // not a prefab no = null; return false; } var go = AssetDatabase.LoadAssetAtPath(path); if (!go) { no = null; return false; } no = go.GetComponent(); return no; } void OnPostprocessPrefab(GameObject prefab) { var no = prefab.GetComponent(); if (no && no.IsSpawnable) { var existing = prefab.GetComponent(); if (existing != null) { // this is likely a variant prefab, can't add the next one // also, component loses hide flags at this point, so they need to be restored // weirdly, this is the only case where altering a component in OnPostprocessPrefab works // without causing an import warning existing.Guid = NetworkObjectGuid.Parse(AssetDatabase.AssetPathToGUID(context.assetPath)); existing.hideFlags = HideFlags.DontSaveInEditor | HideFlags.HideInInspector | HideFlags.NotEditable; } else { var indirect = prefab.AddComponent(); indirect.Guid = NetworkObjectGuid.Parse(AssetDatabase.AssetPathToGUID(context.assetPath)); indirect.hideFlags |= HideFlags.HideInInspector | HideFlags.NotEditable; } } } static bool BakePrefab(string prefabPath, out GameObject root) { root = null; var assetGuid = AssetDatabase.AssetPathToGUID(prefabPath); if (!NetworkObjectGuid.TryParse(assetGuid, out var guid)) { FusionEditorLog.ErrorImport(prefabPath, $"Unable to parse guid: \"{assetGuid}\", not going to bake"); return false; } var stageGo = PrefabUtility.LoadPrefabContents(prefabPath); if (!stageGo) { FusionEditorLog.ErrorImport(prefabPath, $"Unable to load prefab contents"); return false; } var sw = System.Diagnostics.Stopwatch.StartNew(); try { bool dirty = false; bool baked = false; if (OnBakePrefab != null) { var args = new NetworkObjectBakePrefabArgs(_baker, stageGo, prefabPath); OnBakePrefab(args); if (args.Handled) { baked = true; dirty = args.IsPrefabDirty; } } if (!baked) { dirty = _baker.Bake(stageGo).HadChanges; } FusionEditorLog.TraceImport(prefabPath, $"Baking took {sw.Elapsed}, changed: {dirty}"); if (dirty) { root = PrefabUtility.SaveAsPrefabAsset(stageGo, prefabPath); } return root; } finally { PrefabUtility.UnloadPrefabContents(stageGo); } } private static NetworkObjectBaker _baker = new NetworkObjectBakerEditTime(); private static void OnPlaymodeChange(PlayModeStateChange change) { if (change != PlayModeStateChange.ExitingEditMode) { return; } for (int i = 0; i < EditorSceneManager.sceneCount; ++i) { BakeScene(EditorSceneManager.GetSceneAt(i)); } } private static void OnSceneSaving(Scene scene, string path) { BakeScene(scene); } [MenuItem("Tools/Fusion/Scene/Bake Scene Objects", false, FusionAssistants.PRIORITY_LOW - 1)] [MenuItem("GameObject/Fusion/Scene/Bake Scene Objects", false, FusionAssistants.PRIORITY - 1)] public static void BakeAllOpenScenes() { for (int i = 0; i < SceneManager.sceneCount; ++i) { var scene = SceneManager.GetSceneAt(i); try { BakeScene(scene); } catch (Exception ex) { Debug.LogError($"Failed to bake scene {scene}: {ex}"); } } } public static void BakeScene(Scene scene) { var sw = System.Diagnostics.Stopwatch.StartNew(); try { if (OnBakeScene != null) { var args = new NetworkObjectBakeSceneArgs(_baker, scene); OnBakeScene(args); if (args.Handled) { return; } } foreach (var root in scene.GetRootGameObjects()) { _baker.Bake(root); } } finally { FusionEditorLog.TraceImport(scene.path, $"Baking {scene} took: {sw.Elapsed}"); } } } public class NetworkObjectBakePrefabArgs { public bool IsPrefabDirty { get; set; } public bool Handled { get; set; } public GameObject LoadedPrefabRoot { get; } public string Path { get; } public NetworkObjectBaker Baker { get; } public NetworkObjectBakePrefabArgs(NetworkObjectBaker baker, GameObject loadedPrefabRoot, string path) { LoadedPrefabRoot = loadedPrefabRoot; Path = path; Baker = baker; } } public class NetworkObjectBakeSceneArgs { public bool Handled { get; set; } public Scene Scene { get; } public NetworkObjectBaker Baker { get; } public NetworkObjectBakeSceneArgs(NetworkObjectBaker baker, Scene scene) { Scene = scene; Baker = baker; } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkPrefabsInspector.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; using Object = UnityEngine.Object; public class NetworkPrefabsInspector : EditorWindow { private Grid _grid = new Grid(); [MenuItem("Tools/Fusion/Windows/Network Prefabs Inspector")] [MenuItem("Window/Fusion/Network Prefabs Inspector")] public static void ShowWindow() { var window = GetWindow(false, "Network Prefabs Inspector"); window.Show(); } private void OnEnable() { _grid.PrefabTable = NetworkProjectConfig.Global.PrefabTable; _grid.OnEnable(); } private void OnInspectorUpdate() { _grid.OnInspectorUpdate(); } private void OnGUI() { using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { _grid.DrawToolbarReloadButton(); _grid.DrawToolbarSyncSelectionButton(); GUILayout.FlexibleSpace(); EditorGUI.BeginChangeCheck(); _grid.OnlyLoaded = GUILayout.Toggle(_grid.OnlyLoaded, "Loaded Only", EditorStyles.toolbarButton); if (EditorGUI.EndChangeCheck()) { _grid.ResetTree(); } _grid.DrawToolbarSearchField(); } var rect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); _grid.OnGUI(rect); } private enum LoadState { NotLoaded, Loading, LoadedNoInstances, Loaded } [Serializable] private class InspectorTreeViewState : TreeViewState { public MultiColumnHeaderState HeaderState; public bool SyncSelection; } private class GridItem : FusionGridItem { private readonly NetworkPrefabId _prefabId; private readonly NetworkPrefabTable _prefabTable; public GridItem(NetworkPrefabTable prefabTable, NetworkPrefabId prefabId) { _prefabId = prefabId; _prefabTable = prefabTable; } public int InstanceCount => _prefabTable.GetInstancesCount(_prefabId); public string Path => AssetDatabase.GUIDToAssetPath(Guid); public string Guid => Source?.AssetGuid.ToUnityGuidString() ?? "Null"; public override Object TargetObject { get { if (Source?.AssetGuid.IsValid == true) { if (NetworkProjectConfigUtilities.TryGetPrefabEditorInstance(Source.AssetGuid, out var result)) { return result.gameObject; } } return null; } } public INetworkPrefabSource Source => _prefabTable.GetSource(_prefabId); public string Description { get => Source?.Description ?? "Null"; } public LoadState LoadState { get { if (!_prefabTable.IsAcquired(_prefabId)) { return LoadState.NotLoaded; } if (!_prefabTable.GetSource(_prefabId).IsCompleted) { return LoadState.Loading; } if (_prefabTable.GetInstancesCount(_prefabId) == 0) { return LoadState.LoadedNoInstances; } return LoadState.Loaded; } } public NetworkPrefabId PrefabId => _prefabId; } [Serializable] class Grid : FusionGrid { [SerializeField] public NetworkPrefabTable PrefabTable; [SerializeField] public bool OnlyLoaded; public override int GetContentHash() { return PrefabTable?.Version ?? 0; } protected override IEnumerable CreateColumns() { yield return new() { headerContent = new GUIContent("State"), width = 40, autoResize = false, cellGUI = (item, rect, _, _) => { var icon = FusionEditorSkin.LoadStateIcon; string label = ""; Color color; switch (item.LoadState) { case LoadState.Loaded: color = Color.green; label = item.InstanceCount.ToString(); break; case LoadState.LoadedNoInstances: color = Color.yellow; label = "0"; break; case LoadState.Loading: color = Color.yellow; color.a = 0.5f; label = "0"; break; default: color = Color.gray; break; } using (new FusionEditorGUI.ContentColorScope(color)) { EditorGUI.LabelField(rect, new GUIContent(label, icon, item.LoadState.ToString())); } }, getComparer = order => (a, b) => { var result = a.LoadState.CompareTo(b.LoadState) * order; if (result != 0) { return result; } return a.InstanceCount.CompareTo(b.InstanceCount) * order; }, }; yield return new() { headerContent = new GUIContent("Type"), width = 40, maxWidth = 40, minWidth = 40, cellGUI = (item, rect, _, _) => INetworkPrefabSourceDrawer.DrawThumbnail(rect, item.Source), getComparer = order => (a, b) => EditorUtility.NaturalCompare(a.Source?.GetType().Name ?? "", b.Source?.GetType().Name ?? "") * order, }; yield return MakeSimpleColumn(x => x.PrefabId, new() { cellGUI = (item, rect, selected, focused) => TreeView.DefaultGUI.Label(rect, item.PrefabId.ToString(false, false), selected , focused), width = 50, autoResize = false }); yield return MakeSimpleColumn(x => x.Path, new() { initiallySorted = true, }); yield return MakeSimpleColumn(x => x.Guid, new() { initiallyVisible = false }); yield return MakeSimpleColumn(x => x.Description, new() { initiallyVisible = false }); } protected override IEnumerable CreateRows() { if (PrefabTable == null) { yield break; } for (int i = 0; i < PrefabTable.Prefabs.Count; ++i) { var prefabId = NetworkPrefabId.FromIndex(i); if (OnlyLoaded && !PrefabTable.IsAcquired(prefabId)) { continue; } yield return new GridItem(PrefabTable, NetworkPrefabId.FromIndex(i)) { id = (int)(i + 1) }; } } protected override GenericMenu CreateContextMenu(GridItem item, TreeView treeView) { var menu = new GenericMenu(); var selection = treeView.GetSelection() .Select(x => NetworkPrefabId.FromIndex(x-1)) .ToList(); var anyLoaded = selection.Any(x => PrefabTable.IsAcquired(x)); var anyNotLoaded = selection.Any(x => !PrefabTable.IsAcquired(x)); var anyInstances = selection.Any(x => PrefabTable.GetInstancesCount(x) > 0); var spawnerRunners = NetworkRunner.Instances.Where(x => x && x.IsRunning && x.CanSpawn).ToArray(); var loadContent = new GUIContent("Load"); var loadAsyncContent = new GUIContent("Load (async)"); var unloadContent = new GUIContent("Unload"); var selectInstancesContent = new GUIContent("Select Instances"); var spawnContent = new GUIContent("Spawn"); var spawnAsyncContent = new GUIContent("Spawn (async)"); if (anyNotLoaded) { menu.AddItem(loadContent, false, () => { foreach (var id in selection) { PrefabTable.Load(id, isSynchronous: true); } }); menu.AddItem(loadAsyncContent, false, () => { foreach (var id in selection) { PrefabTable.Load(id, isSynchronous: false); } }); } else { menu.AddDisabledItem(loadContent); menu.AddDisabledItem(loadAsyncContent); } if (anyLoaded) { menu.AddItem(unloadContent, false, () => { foreach (var id in selection) { PrefabTable.Unload(id); } }); } else { menu.AddDisabledItem(unloadContent); } if (anyInstances) { menu.AddItem(selectInstancesContent, false, () => { var lookup = new HashSet(selection.Select(x => NetworkObjectTypeId.FromPrefabId(x))); Selection.objects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None) .Where(x => x.NetworkTypeId.IsValid && lookup.Contains(x.NetworkTypeId)) .Select(x => x.gameObject) .ToArray(); }); } else { menu.AddDisabledItem(selectInstancesContent); } menu.AddSeparator(""); if (spawnerRunners.Any()) { if (spawnerRunners.Length > 1) { foreach (var runner in spawnerRunners.Where(x => x.CanSpawn)) { AddSpawnItems($"/{runner.name}", runner); } } else { AddSpawnItems($"", spawnerRunners[0]); } } else { menu.AddDisabledItem(spawnContent); menu.AddDisabledItem(spawnAsyncContent); } void AddSpawnItems(string s, NetworkRunner networkRunner) { menu.AddItem(new GUIContent($"{spawnContent.text}{s}"), false, () => { foreach (var id in selection) { networkRunner.TrySpawn(id, out _); } }); menu.AddItem(new GUIContent($"{spawnAsyncContent.text}{s}"), false, () => { foreach (var id in selection) { networkRunner.SpawnAsync(id); } }); } return menu; } } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkPrefabSourceFactories.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; partial interface INetworkAssetSourceFactory { INetworkPrefabSource TryCreatePrefabSource(in NetworkAssetSourceFactoryContext context); } public class NetworkAssetSourceFactory { private readonly List _factories = TypeCache.GetTypesDerivedFrom() .Select(x => (INetworkAssetSourceFactory)Activator.CreateInstance(x)) .OrderBy(x => x.Order) .ToList(); public INetworkPrefabSource TryCreatePrefabSource(in NetworkAssetSourceFactoryContext context, bool removeFaultedFactories = true) { for (int i = 0; i < _factories.Count; ++i) { var factory = _factories[i]; try { var source = factory.TryCreatePrefabSource(in context); if (source != null) { return source; } } catch (Exception ex) when(removeFaultedFactories) { FusionEditorLog.Error($"Prefab source factory {factory.GetType().Name} failed for {context.AssetPath}. " + $"This factory will be removed from the list of available factories during this import." + $"Reimport of fix the underlying issue: {ex}"); } } return null; } } partial class NetworkAssetSourceFactoryStatic { public INetworkPrefabSource TryCreatePrefabSource(in NetworkAssetSourceFactoryContext context) { if (TryCreateInternal(context, out var result)) { result.AssetGuid = NetworkObjectGuid.Parse(context.AssetGuid); }; return result; } } partial class NetworkAssetSourceFactoryResource { public INetworkPrefabSource TryCreatePrefabSource(in NetworkAssetSourceFactoryContext context) { if (TryCreateInternal(context, out var result)) { result.AssetGuid = NetworkObjectGuid.Parse(context.AssetGuid); }; return result; } } #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES partial class NetworkAssetSourceFactoryAddressable { public INetworkPrefabSource TryCreatePrefabSource(in NetworkAssetSourceFactoryContext context) { if (TryCreateInternal(context, out var result)) { result.AssetGuid = NetworkObjectGuid.Parse(context.AssetGuid); }; return result; } } #endif } #endregion #region Assets/Photon/Fusion/Editor/NetworkRunnerEditor.cs namespace Fusion.Editor { using System; using System.Linq; using System.Runtime.InteropServices; using UnityEditor; using UnityEngine; [CustomEditor(typeof(NetworkRunner))] public class NetworkRunnerEditor : BehaviourEditor { void Label(string label, T value) { EditorGUILayout.LabelField(label, (value != null ? value.ToString() : "null")); } public override void OnInspectorGUI() { base.OnInspectorGUI(); var runner = target as NetworkRunner; if (runner && EditorApplication.isPlaying) { Label("State", runner.IsRunning ? "Running" : (runner.IsShutdown ? "Shutdown" : "None")); if (runner.IsRunning) { Label("Game Mode", runner.GameMode); Label("Simulation Mode", runner.Mode); Label("Is Player", runner.IsPlayer); Label("Local Player", runner.LocalPlayer); Label("Has Connection Token?", runner.GetPlayerConnectionToken() != null); var localplayerobj = runner.LocalPlayer.IsRealPlayer ? runner.GetPlayerObject(runner.LocalPlayer) : null; EditorGUILayout.ObjectField("Local PlayerObject", localplayerobj, typeof(NetworkObject), true); Label("Is SinglePlayer", runner.IsSinglePlayer); if (runner.TryGetSceneInfo(out var sceneInfo)) { Label("Scene Info", sceneInfo); } else { Label("Scene Info", $"Invalid"); } var playerCount = runner.ActivePlayers.Count(); Label("Active Players", playerCount); if (runner.IsServer && playerCount > 0) { foreach (var item in runner.ActivePlayers) { // skip local player if (runner.LocalPlayer == item) { continue; } Label("Player:PlayerId", item.PlayerId); Label("Player:ConnectionType", runner.GetPlayerConnectionType(item)); Label("Player:UserId", runner.GetPlayerUserId(item)); Label("Player:RTT", runner.GetPlayerRtt(item)); } } if (runner.IsClient) { Label("Is Connected To Server", runner.IsConnectedToServer); Label("Current Connection Type", runner.CurrentConnectionType); } } Label("Is Cloud Ready", runner.IsCloudReady); if (runner.IsCloudReady) { Label("Is Shared Mode Master Client", runner.IsSharedModeMasterClient); Label("UserId", runner.UserId); Label("AuthenticationValues", runner.AuthenticationValues); Label("SessionInfo:IsValid", runner.SessionInfo.IsValid); if (runner.SessionInfo.IsValid) { Label("SessionInfo:Name", runner.SessionInfo.Name); Label("SessionInfo:IsVisible", runner.SessionInfo.IsVisible); Label("SessionInfo:IsOpen", runner.SessionInfo.IsOpen); Label("SessionInfo:Region", runner.SessionInfo.Region); } Label("LobbyInfo:IsValid", runner.LobbyInfo.IsValid); if (runner.LobbyInfo.IsValid) { Label("LobbyInfo:Name", runner.LobbyInfo.Name); Label("LobbyInfo:Region", runner.LobbyInfo.Region); } } } else { if (runner.TryGetComponent(out var _) == false) { EditorGUILayout.Space(2); if (GUI.Button(EditorGUILayout.GetControlRect(), $"Add {nameof(RunnerEnableVisibility)}")) { runner.gameObject.AddComponent(); } } } } } } #endregion #region Assets/Photon/Fusion/Editor/NetworkSceneDebugStartEditor.cs // file deleted #endregion #region Assets/Photon/Fusion/Editor/NetworkTRSPEditor.cs namespace Fusion.Editor { using UnityEditor; [CustomEditor(typeof(NetworkTRSP), true)] public unsafe class NetworkTRSPEditor : NetworkBehaviourEditor { public override void OnInspectorGUI() { base.OnInspectorGUI(); var t = (NetworkTRSP)target; using (new EditorGUI.DisabledScope(true)) { if (t && t.StateBufferIsValid && t.CanReceiveRenderCallback) { var found = t.Runner.TryFindObject(t.Data.Parent.Object, out var parent); EditorGUILayout.LabelField("Parent", $"'{(found ? parent.name : "Not Available")}' : {t.Data.Parent.Object.ToString()}"); } } } } } #endregion #region Assets/Photon/Fusion/Editor/PhotonAppSettingsEditor.cs namespace Fusion.Editor { using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using Photon.Realtime; [CustomEditor(typeof(PhotonAppSettings))] public class PhotonAppSettingsEditor : Editor { public override void OnInspectorGUI() { FusionEditorGUI.InjectScriptHeaderDrawer(serializedObject); base.DrawDefaultInspector(); } [MenuItem("Tools/Fusion/Realtime Settings", priority = 200)] public static void PingNetworkProjectConfigAsset() { EditorGUIUtility.PingObject(PhotonAppSettings.Global); Selection.activeObject = PhotonAppSettings.Global; } } } #endregion #region Assets/Photon/Fusion/Editor/Utilities/AnimatorControllerTools.cs // --------------------------------------------------------------------------------------------- // PhotonNetwork Framework for Unity - Copyright (C) 2020 Exit Games GmbH // developer@exitgames.com // --------------------------------------------------------------------------------------------- namespace Fusion.Editor { using System.Collections.Generic; using UnityEngine; using UnityEditor.Animations; using UnityEditor; /// /// Storage type for AnimatorController cached transition data, which is a bit different than basic state hashes /// [System.Serializable] public class TransitionInfo { public int index; public int hash; public int state; public int destination; public float duration; public float offset; public bool durationIsFixed; public TransitionInfo(int index, int hash, int state, int destination, float duration, float offset, bool durationIsFixed) { this.index = index; this.hash = hash; this.state = state; this.destination = destination; this.duration = duration; this.offset = offset; this.durationIsFixed = durationIsFixed; } } public static class AnimatorControllerTools { //// Attach methods to Fusion.Runtime NetworkedAnimator //[InitializeOnLoadMethod] //public static void RegisterFusionDelegates() { // NetworkedAnimator.GetWordCountDelegate = GetWordCount; //} private static AnimatorController GetController(this Animator a) { RuntimeAnimatorController rac = a.runtimeAnimatorController; AnimatorOverrideController overrideController = rac as AnimatorOverrideController; /// recurse until no override controller is found while (overrideController != null) { rac = overrideController.runtimeAnimatorController; overrideController = rac as AnimatorOverrideController; } return rac as AnimatorController; } private static void GetTriggerNames(this AnimatorController ctr, List namelist) { namelist.Clear(); foreach (var p in ctr.parameters) if (p.type == AnimatorControllerParameterType.Trigger) { if (namelist.Contains(p.name)) { Debug.LogWarning("Identical Trigger Name Found. Check animator on '" + ctr.name + "' for repeated trigger names."); } else namelist.Add(p.name); } } private static void GetTriggerNames(this AnimatorController ctr, List hashlist) { hashlist.Clear(); foreach (var p in ctr.parameters) if (p.type == AnimatorControllerParameterType.Trigger) { hashlist.Add(Animator.StringToHash(p.name)); } } /// ------------------------------ STATES -------------------------------------- private static void GetStatesNames(this AnimatorController ctr, List namelist) { namelist.Clear(); foreach (var l in ctr.layers) { var states = l.stateMachine.states; ExtractNames(ctr, l.name, states, namelist); var substates = l.stateMachine.stateMachines; ExtractSubNames(ctr, l.name, substates, namelist); } } private static void ExtractSubNames(AnimatorController ctr, string path, ChildAnimatorStateMachine[] substates, List namelist) { foreach (var s in substates) { var sm = s.stateMachine; var subpath = path + "." + sm.name; ExtractNames(ctr, subpath, s.stateMachine.states, namelist); ExtractSubNames(ctr, subpath, s.stateMachine.stateMachines, namelist); } } private static void ExtractNames(AnimatorController ctr, string path, ChildAnimatorState[] states, List namelist) { foreach (var st in states) { string name = st.state.name; string layerName = path + "." + st.state.name; if (!namelist.Contains(name)) { namelist.Add(name); } if (namelist.Contains(layerName)) { Debug.LogWarning("Identical State Name '" + st.state.name + "' Found. Check animator on '" + ctr.name + "' for repeated State names as they cannot be used nor networked."); } else namelist.Add(layerName); } } private static void GetStatesNames(this AnimatorController ctr, List hashlist) { hashlist.Clear(); foreach (var l in ctr.layers) { var states = l.stateMachine.states; ExtractHashes(ctr, l.name, states, hashlist); var substates = l.stateMachine.stateMachines; ExtractSubtHashes(ctr, l.name, substates, hashlist); } } private static void ExtractSubtHashes(AnimatorController ctr, string path, ChildAnimatorStateMachine[] substates, List hashlist) { foreach (var s in substates) { var sm = s.stateMachine; var subpath = path + "." + sm.name; ExtractHashes(ctr, subpath, sm.states, hashlist); ExtractSubtHashes(ctr, subpath, sm.stateMachines, hashlist); } } private static void ExtractHashes(AnimatorController ctr, string path, ChildAnimatorState[] states, List hashlist) { foreach (var st in states) { int hash = Animator.StringToHash(st.state.name); string fullname = path + "." + st.state.name; int layrhash = Animator.StringToHash(fullname); if (!hashlist.Contains(hash)) { hashlist.Add(hash); } if (hashlist.Contains(layrhash)) { Debug.LogWarning("Identical State Name '" + st.state.name + "' Found. Check animator on '" + ctr.name + "' for repeated State names as they cannot be used nor networked."); } else hashlist.Add(layrhash); } } //public static void GetTransitionNames(this AnimatorController ctr, List transInfo) //{ // transInfo.Clear(); // transInfo.Add("0"); // foreach (var l in ctr.layers) // { // foreach (var st in l.stateMachine.states) // { // string sname = l.name + "." + st.state.name; // foreach (var t in st.state.transitions) // { // string dname = l.name + "." + t.destinationState.name; // string name = (sname + " -> " + dname); // transInfo.Add(name); // //Debug.Log(sname + " -> " + dname + " " + Animator.StringToHash(sname + " -> " + dname)); // } // } // } //} //public static void GetTransitions(this AnimatorController ctr, List transInfo) //{ // transInfo.Clear(); // transInfo.Add(new TransitionInfo(0, 0, 0, 0, 0, 0, false)); // int index = 1; // foreach (var l in ctr.layers) // { // foreach (var st in l.stateMachine.states) // { // string sname = l.name + "." + st.state.name; // int shash = Animator.StringToHash(sname); // foreach (var t in st.state.transitions) // { // string dname = l.name + "." + t.destinationState.name; // int dhash = Animator.StringToHash(dname); // int hash = Animator.StringToHash(sname + " -> " + dname); // TransitionInfo ti = new TransitionInfo(index, hash, shash, dhash, t.duration, t.offset, t.hasFixedDuration); // transInfo.Add(ti); // //Debug.Log(index + " " + sname + " -> " + dname + " " + Animator.StringToHash(sname + " -> " + dname)); // index++; // } // } // } //} const double AUTO_REBUILD_RATE = 10f; private static List tempNamesList = new List(); private static List tempHashList = new List(); // This method is a near copy of the code used in NMA for determining WordCount, but uses GetController instead. internal static (int paramCount, int boolCount, int layerCount, int words) GetWordCount(this NetworkMecanimAnimator netAnim) { // always get new Animator in case it has changed. Animator animator = netAnim.Animator; if (animator == null) { animator = netAnim.GetComponent(); if (animator == null) { return default; } // Add the animator we found netAnim.Animator = animator; } AnimatorController ac = animator.GetController(); if (ac == null) { return default; } var settings = netAnim.SyncSettings; int param32Count = 0; int paramBoolCount = 0; bool includeI = (settings & AnimatorSyncSettings.ParameterInts) == AnimatorSyncSettings.ParameterInts; bool includeF = (settings & AnimatorSyncSettings.ParameterFloats) == AnimatorSyncSettings.ParameterFloats; bool includeB = (settings & AnimatorSyncSettings.ParameterBools) == AnimatorSyncSettings.ParameterBools; bool includeT = (settings & AnimatorSyncSettings.ParameterTriggers) == AnimatorSyncSettings.ParameterTriggers; var includeStat = (settings & AnimatorSyncSettings.StateRoot) == AnimatorSyncSettings.StateRoot; var includeWght = (settings & AnimatorSyncSettings.LayerWeights) == AnimatorSyncSettings.LayerWeights; var includeLyrs = (settings & AnimatorSyncSettings.StateLayers) == AnimatorSyncSettings.StateLayers; var parameters = ac.parameters; for (int i = 0; i < parameters.Length; ++i) { var param = parameters[i]; switch (param.type) { case AnimatorControllerParameterType.Int: if (includeI) param32Count++; break; case AnimatorControllerParameterType.Float: if (includeF) param32Count++; break; case AnimatorControllerParameterType.Bool: if (includeB) paramBoolCount++; break; case AnimatorControllerParameterType.Trigger: if (includeT) paramBoolCount++; break; } } int layerCount = ac.layers.Length; int syncedLayerCount = includeLyrs ? layerCount : 1; int stateWordCount = includeStat ? 2 * syncedLayerCount : 0; int weightWordCount = (includeWght && layerCount > 0) ? (layerCount - 1) : 0; int paramBoolsWordCount = (paramBoolCount + 31) >> 5; int words = param32Count + paramBoolsWordCount + stateWordCount + weightWordCount; return (param32Count, paramBoolCount, layerCount, words); } /// /// Re-index all of the State and Trigger names in the current AnimatorController. Never hurts to run this (other than hanging the editor for a split second). /// internal static void GetHashesAndNames(this NetworkMecanimAnimator netAnim, List sharedTriggNames, List sharedStateNames, ref int[] sharedTriggIndexes, ref int[] sharedStateIndexes //ref double lastRebuildTime ) { // always get new Animator in case it has changed. Animator animator = netAnim.Animator; if (animator == null) animator = netAnim.GetComponent(); if (animator == null) { return; } //if (animator && EditorApplication.timeSinceStartup - lastRebuildTime > AUTO_REBUILD_RATE) { // lastRebuildTime = EditorApplication.timeSinceStartup; AnimatorController ac = animator.GetController(); if (ac != null) { if (ac.animationClips == null || ac.animationClips.Length == 0) Debug.LogWarning("'" + animator.name + "' has an Animator with no animation clips. Some Animator Controllers require a restart of Unity, or for a Build to be made in order to initialize correctly."); bool haschanged = false; ac.GetTriggerNames(tempHashList); tempHashList.Insert(0, 0); if (!CompareIntArray(sharedTriggIndexes, tempHashList)) { sharedTriggIndexes = tempHashList.ToArray(); haschanged = true; } ac.GetStatesNames(tempHashList); tempHashList.Insert(0, 0); if (!CompareIntArray(sharedStateIndexes, tempHashList)) { sharedStateIndexes = tempHashList.ToArray(); haschanged = true; } if (sharedTriggNames != null) { ac.GetTriggerNames(tempNamesList); tempNamesList.Insert(0, null); if (!CompareNameLists(tempNamesList, sharedTriggNames)) { CopyNameList(tempNamesList, sharedTriggNames); haschanged = true; } } if (sharedStateNames != null) { ac.GetStatesNames(tempNamesList); tempNamesList.Insert(0, null); if (!CompareNameLists(tempNamesList, sharedStateNames)) { CopyNameList(tempNamesList, sharedStateNames); haschanged = true; } } if (haschanged) { Debug.Log(animator.name + " has changed. SyncAnimator indexes updated."); EditorUtility.SetDirty(netAnim); } } //} } private static bool CompareNameLists(List one, List two) { if (one.Count != two.Count) return false; for (int i = 0; i < one.Count; i++) if (one[i] != two[i]) return false; return true; } private static bool CompareIntArray(int[] old, List temp) { if (ReferenceEquals(old, null)) return false; if (old.Length != temp.Count) return false; for (int i = 0; i < old.Length; i++) if (old[i] != temp[i]) return false; return true; } private static void CopyNameList(List src, List trg) { trg.Clear(); for (int i = 0; i < src.Count; i++) trg.Add(src[i]); } } } #endregion #region Assets/Photon/Fusion/Editor/Utilities/AssetDatabaseUtils.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEditor; #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else using UnityEditor.Experimental.SceneManagement; #endif using UnityEngine; public static partial class AssetDatabaseUtils { public static T GetSubAsset(GameObject prefab) where T : ScriptableObject { if (!AssetDatabase.IsMainAsset(prefab)) { throw new InvalidOperationException($"Not a main asset: {prefab}"); } string path = AssetDatabase.GetAssetPath(prefab); if (string.IsNullOrEmpty(path)) { throw new InvalidOperationException($"Empty path for prefab: {prefab}"); } var subAssets = AssetDatabase.LoadAllAssetsAtPath(path).OfType().ToList(); if (subAssets.Count > 1) { Debug.LogError($"More than 1 asset of type {typeof(T)} on {path}, clean it up manually"); } return subAssets.Count == 0 ? null : subAssets[0]; } public static bool IsSceneObject(GameObject go) { return ReferenceEquals(PrefabStageUtility.GetPrefabStage(go), null) && (PrefabUtility.IsPartOfPrefabAsset(go) == false || PrefabUtility.GetPrefabAssetType(go) == PrefabAssetType.NotAPrefab); } } } #endregion #region Assets/Photon/Fusion/Editor/Utilities/FusionEditorGUI.cs namespace Fusion.Editor { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEngine; public static partial class FusionEditorGUI { public static void LayoutSelectableLabel(GUIContent label, string contents) { var rect = EditorGUILayout.GetControlRect(); rect = EditorGUI.PrefixLabel(rect, label); using (new EditorGUI.IndentLevelScope(-EditorGUI.indentLevel)) { EditorGUI.SelectableLabel(rect, contents); } } public static bool DrawDefaultInspector(SerializedObject obj, bool drawScript = true) { EditorGUI.BeginChangeCheck(); obj.UpdateIfRequiredOrScript(); // Loop through properties and create one field (including children) for each top level property. SerializedProperty property = obj.GetIterator(); bool expanded = true; while (property.NextVisible(expanded)) { if ( ScriptPropertyName == property.propertyPath ) { if (drawScript) { using (new EditorGUI.DisabledScope("m_Script" == property.propertyPath)) { EditorGUILayout.PropertyField(property, true); } } } else { EditorGUILayout.PropertyField(property, true); } expanded = false; } obj.ApplyModifiedProperties(); return EditorGUI.EndChangeCheck(); } } } #endregion #region Assets/Photon/Fusion/Editor/Utilities/FusionEditorGUI.Thumbnail.cs namespace Fusion.Editor { using System; using System.Text; using UnityEngine; public static partial class FusionEditorGUI { static readonly int _thumbnailFieldHash = "Thumbnail".GetHashCode(); static Texture2D _thumbnailBackground; static GUIStyle _thumbnailStyle; public static void DrawTypeThumbnail(Rect position, Type type, string prefixToSkip, string tooltip = null) { EnsureThumbnailStyles(); var acronym = GenerateAcronym(type, prefixToSkip); var content = new GUIContent(acronym, tooltip ?? type.FullName); int controlID = GUIUtility.GetControlID(_thumbnailFieldHash, FocusType.Passive, position); if (Event.current.type == EventType.Repaint) { var originalColor = GUI.backgroundColor; try { GUI.backgroundColor = GetPersistentColor(type.FullName); _thumbnailStyle.fixedWidth = position.width; _thumbnailStyle.Draw(position, content, controlID); } finally { GUI.backgroundColor = originalColor; } } } static Color GetPersistentColor(string str) { return GeneratePastelColor(HashCodeUtilities.GetHashDeterministic(str)); } static Color GeneratePastelColor(int seed) { var rng = new System.Random(seed); int r = rng.Next(256) + 128; int g = rng.Next(256) + 128; int b = rng.Next(256) + 128; r = Mathf.Min(r / 2, 255); g = Mathf.Min(g / 2, 255); b = Mathf.Min(b / 2, 255); var result = new Color32((byte)r, (byte)g, (byte)b, 255); return result; } static string GenerateAcronym(Type type, string prefixToStrip) { StringBuilder acronymBuilder = new StringBuilder(); var str = type.Name; if (!string.IsNullOrEmpty(prefixToStrip)) { if (str.StartsWith(prefixToStrip)) { str = str.Substring(prefixToStrip.Length); } } for (int i = 0; i < str.Length; ++i) { var c = str[i]; if (i != 0 && char.IsLower(c)) { continue; } acronymBuilder.Append(c); } return acronymBuilder.ToString(); } static void EnsureThumbnailStyles() { if (_thumbnailBackground != null) { return; } byte[] data = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x08, 0x06, 0x00, 0x00, 0x00, 0x8d, 0x89, 0x1d, 0x0d, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, 0x00, 0x00, 0xf2, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0xed, 0x95, 0x31, 0x0a, 0x83, 0x30, 0x14, 0x86, 0x63, 0x11, 0x74, 0x50, 0x74, 0x71, 0xf1, 0x34, 0x01, 0x57, 0x6f, 0xe8, 0xe0, 0xd0, 0xa5, 0x07, 0x10, 0x0a, 0xbd, 0x40, 0x0f, 0xe2, 0xa8, 0x9b, 0xee, 0xf6, 0x7d, 0x69, 0x4a, 0xa5, 0xd2, 0x2a, 0xa6, 0x4b, 0xa1, 0x1f, 0x04, 0x5e, 0xc2, 0xff, 0xbe, 0x68, 0x90, 0xa8, 0x5e, 0xd0, 0x69, 0x9a, 0x9e, 0xc3, 0x30, 0x1c, 0xa4, 0x9e, 0x3e, 0x0d, 0x32, 0x64, 0xa5, 0xd6, 0x32, 0x16, 0xf8, 0x51, 0x14, 0x1d, 0xb3, 0x2c, 0x1b, 0xab, 0xaa, 0x9a, 0xda, 0xb6, 0x9d, 0xd6, 0x20, 0x43, 0x96, 0x1e, 0x7a, 0x71, 0xdc, 0x55, 0x02, 0x0b, 0x45, 0x51, 0x0c, 0x82, 0x8d, 0x6f, 0x87, 0x1e, 0x7a, 0xad, 0xd4, 0xa0, 0xd9, 0x65, 0x8f, 0xec, 0x01, 0xbd, 0x38, 0x70, 0x29, 0xce, 0x81, 0x47, 0x77, 0x05, 0x87, 0x39, 0x53, 0x0e, 0x77, 0xcb, 0x99, 0xad, 0x81, 0x03, 0x97, 0x27, 0x8f, 0xc9, 0xdc, 0xbc, 0xbb, 0x2b, 0x9e, 0xe7, 0xa9, 0x83, 0xad, 0xbf, 0xc6, 0x5f, 0xe8, 0xce, 0x0f, 0x08, 0xe5, 0x63, 0x1c, 0xfb, 0xbe, 0xb7, 0xd3, 0xfd, 0xe0, 0xc0, 0x75, 0x08, 0x82, 0xe0, 0xda, 0x34, 0x8d, 0x5d, 0xde, 0x0f, 0x0e, 0x5c, 0xd4, 0x3a, 0xcf, 0x73, 0xe7, 0xcb, 0x01, 0x07, 0x2e, 0x84, 0x2a, 0x8e, 0xe3, 0x53, 0x59, 0x96, 0xbb, 0xa4, 0xf4, 0xd0, 0x8b, 0xc3, 0xc8, 0x2c, 0x3e, 0x0b, 0xec, 0x52, 0xd7, 0xf5, 0xd4, 0x75, 0x9d, 0x8d, 0xbf, 0x87, 0x0c, 0x59, 0x7a, 0xac, 0xec, 0x79, 0xc1, 0xce, 0xd0, 0x49, 0x92, 0x5c, 0xb8, 0x35, 0xa4, 0x5e, 0x5c, 0xfb, 0xf3, 0x41, 0x86, 0xac, 0xd4, 0xb3, 0x5f, 0x80, 0x52, 0x37, 0xfd, 0x56, 0x1b, 0x09, 0x40, 0x56, 0xe4, 0x85, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 }; var texture = new Texture2D(2, 2, TextureFormat.ARGB32, false); if (!texture.LoadImage(data)) { throw new InvalidOperationException(); } _thumbnailBackground = texture; _thumbnailStyle = new GUIStyle() { normal = new GUIStyleState { background = _thumbnailBackground, textColor = Color.white }, border = new RectOffset(6, 6, 6, 6), padding = new RectOffset(2, 1, 1, 1), imagePosition = ImagePosition.TextOnly, alignment = TextAnchor.MiddleCenter, clipping = TextClipping.Clip, wordWrap = true, stretchWidth = false, fontSize = 8, fontStyle = FontStyle.Bold, fixedWidth = texture.width, }; } } } #endregion #region Assets/Photon/Fusion/Editor/Utilities/NetworkProjectConfigUtilities.cs namespace Fusion.Editor { using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using System.Collections.Generic; using Fusion.Photon.Realtime; using System.Linq; using System.IO; using System; /// /// Editor utilities for creating and managing the singleton. /// [InitializeOnLoad] public static class NetworkProjectConfigUtilities { // Constructor runs on project load, allows for startup check for existence of NPC asset. static NetworkProjectConfigUtilities() { EditorApplication.playModeStateChanged += (change) => { if (change == PlayModeStateChange.EnteredEditMode) { NetworkProjectConfig.UnloadGlobal(); } }; } [MenuItem("Tools/Fusion/Network Project Config", priority = 200)] [MenuItem("Assets/Create/Fusion/Network Project Config", priority = 0)] static void PingNetworkProjectConfigAsset() { FusionGlobalScriptableObjectUtils.EnsureAssetExists(); NetworkProjectConfigUtilities.PingGlobalConfigAsset(true); } [MenuItem("Tools/Fusion/Rebuild Prefab Table", priority = 100)] public static void RebuildPrefabTable() { foreach (var prefab in AssetDatabase.FindAssets($"t:prefab") .Select(AssetDatabase.GUIDToAssetPath) .Select(x => (GameObject)AssetDatabase.LoadMainAssetAtPath(x))) { if (prefab.TryGetComponent(out var networkObject) && !networkObject.Flags.IsIgnored()) { AssetDatabaseUtils.SetLabel(prefab, NetworkProjectConfigImporter.FusionPrefabTag, true); } else { AssetDatabaseUtils.SetLabel(prefab, NetworkProjectConfigImporter.FusionPrefabTag, false); } } AssetDatabase.Refresh(); ImportGlobalConfig(); Debug.Log("Rebuild Prefab Table done."); } public static void PingGlobalConfigAsset(bool select = false) { if (NetworkProjectConfigAsset.TryGetGlobal(out var config)) { EditorGUIUtility.PingObject(config); if (select) { Selection.activeObject = config; } } } public static bool TryGetGlobalPrefabSource(NetworkObjectGuid guid, out T source) where T : class, INetworkPrefabSource { if (NetworkProjectConfigAsset.TryGetGlobal(out var global)) { if (global.Config.PrefabTable.GetSource(guid) is T sourceT) { source = sourceT; return true; } } source = null; return false; } public static bool TryGetPrefabId(NetworkObjectGuid guid, out NetworkPrefabId id) { id = NetworkProjectConfig.Global.PrefabTable.GetId(guid); return id.IsValid; } public static bool TryGetPrefabId(string prefabPath, out NetworkPrefabId id) { var guidStr = AssetDatabase.AssetPathToGUID(prefabPath); if (NetworkObjectGuid.TryParse(guidStr, out var guid)) { return TryGetPrefabId(guid, out id); } id = default; return false; } // public static bool TryResolvePrefab(NetworkObjectGuid guid, out NetworkObject prefab) { // if (TryGetPrefabSource(guid, out NetworkPrefabSourceBase source)) { // try { // prefab = NetworkPrefabSourceFactory.ResolveOrThrow(source); // return true; // } catch (Exception ex) { // FusionEditorLog.Trace(ex.ToString()); // } // } // // prefab = null; // return false; // } internal static bool TryGetPrefabEditorInstance(NetworkObjectGuid guid, out NetworkObject prefab) { if (!guid.IsValid) { prefab = null; return false; } var path = AssetDatabase.GUIDToAssetPath(guid.ToUnityGuidString()); if (string.IsNullOrEmpty(path)) { prefab = null; return false; } var gameObject = AssetDatabase.LoadAssetAtPath(path); if (!gameObject) { prefab = null; return false; } prefab = gameObject.GetComponent(); return prefab; } internal static string GetGlobalConfigPath() { return FusionGlobalScriptableObjectUtils.GetGlobalAssetPath(); } public static bool ImportGlobalConfig() { return FusionGlobalScriptableObjectUtils.TryImportGlobal(); } public static string SaveGlobalConfig() { if (NetworkProjectConfigAsset.TryGetGlobal(out var global)) { return SaveGlobalConfig(global.Config); } else { return SaveGlobalConfig(new NetworkProjectConfig()); } } public static string SaveGlobalConfig(NetworkProjectConfig config) { FusionGlobalScriptableObjectUtils.EnsureAssetExists(); string path = GetGlobalConfigPath(); var json = EditorJsonUtility.ToJson(config, true); string existingJson = File.ReadAllText(path); if (!string.Equals(json, existingJson)) { AssetDatabase.MakeEditable(path); File.WriteAllText(path, json); } AssetDatabase.ImportAsset(path); return PathUtils.Normalize(path); } private static string[] GetEnabledBuildScenes() { var scenes = new List(); for (int i = 0; i < EditorBuildSettings.scenes.Length; ++i) { var scene = EditorBuildSettings.scenes[i]; if (scene.enabled && string.IsNullOrEmpty(scene.path) == false) { scenes.Add(scene.path); } } return scenes.ToArray(); } } } #endregion #region Assets/Photon/Fusion/Editor/Utilities/NetworkRunnerUtilities.cs namespace Fusion.Editor { using System.Collections.Generic; using UnityEngine; using UnityEditor; public static class NetworkRunnerUtilities { static List reusableRunnerList = new List(); public static NetworkRunner[] FindActiveRunners() { var runners = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID); reusableRunnerList.Clear(); for (int i = 0; i < runners.Length; ++i) { if (runners[i].IsRunning) reusableRunnerList.Add(runners[i]); } if (reusableRunnerList.Count == runners.Length) return runners; return reusableRunnerList.ToArray(); } public static void FindActiveRunners(List nonalloc) { var runners = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID); nonalloc.Clear(); for (int i = 0; i < runners.Length; ++i) { if (runners[i].IsRunning) nonalloc.Add(runners[i]); } } } } #endregion #endif