升级水插件

This commit is contained in:
2026-01-31 00:32:49 +08:00
parent a739d2fe3b
commit 4123e83573
293 changed files with 13449 additions and 2853 deletions

View File

@@ -147,6 +147,21 @@ namespace WaveHarmonic.Crest
/// </summary>
sealed class Stripped : DecoratedProperty
{
public enum Style
{
None,
PlatformTab,
}
readonly bool _KeepIndent;
readonly Style _Style;
public Stripped(Style style = Style.None, bool indent = false)
{
_KeepIndent = indent;
_Style = style;
}
internal override bool NeedsControlRectangle(SerializedProperty property) => false;
internal override void OnGUI(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
@@ -155,11 +170,33 @@ namespace WaveHarmonic.Crest
DecoratedDrawer.s_TemporaryColor = true;
DecoratedDrawer.s_PreviousColor = GUI.color;
if (_Style == Style.PlatformTab)
{
EditorGUI.indentLevel += 1;
EditorGUILayout.LabelField(label);
EditorGUI.indentLevel -= 1;
}
GUI.color = new(0, 0, 0, 0);
EditorGUI.indentLevel -= 1;
if (!_KeepIndent) EditorGUI.indentLevel -= 1;
EditorGUI.PropertyField(position, property, label, true);
EditorGUI.indentLevel += 1;
if (!_KeepIndent) EditorGUI.indentLevel += 1;
if (_Style == Style.PlatformTab)
{
EditorGUILayout.Space(4);
EditorGUILayout.EndBuildTargetSelectionGrouping();
}
}
}
sealed class PlatformTabs : DecoratedProperty
{
static readonly GUIContent s_DefaultTab = new(EditorGUIUtility.IconContent("d_Settings").image, "Default");
internal override void OnGUI(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
{
property.intValue = Editor.Reflected.EditorGUILayout.BeginBuildTargetSelectionGrouping(s_DefaultTab);
}
}
@@ -621,29 +658,15 @@ namespace WaveHarmonic.Crest
sealed class InlineToggle : DecoratedProperty
{
// Add extra y offset. Needed for foldouts in foldouts so far.
readonly bool _Fix;
public InlineToggle(bool fix = false)
{
_Fix = fix;
}
internal override bool NeedsControlRectangle(SerializedProperty property)
{
return false;
}
internal override void OnGUI(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
{
var r = position;
r.x -= 16f;
// Align with Space offset.
if (drawer.Space > 0) r.y += drawer.Space + 2f;
if (_Fix) r.y += EditorGUIUtility.singleLineHeight + 2f;
// Seems to be needed.
// Prevent click events blocking next property.
r.width = 16f * (1f + EditorGUI.indentLevel);
r.height = EditorGUIUtility.singleLineHeight;
// Hide text.
label.text = "";
using (new EditorGUI.PropertyScope(r, label, property))
@@ -654,6 +677,9 @@ namespace WaveHarmonic.Crest
property.boolValue = EditorGUI.Toggle(r, property.boolValue);
EditorGUI.EndProperty();
}
// Pull up next property. Extra space might be margin/padding.
GUILayout.Space(-(EditorGUIUtility.singleLineHeight + 2f));
}
}

View File

@@ -11,6 +11,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Events;
using WaveHarmonic.Crest.Attributes;
@@ -23,19 +24,29 @@ namespace WaveHarmonic.Crest.Editor
internal static bool s_HideInInspector = false;
public static bool s_IsFoldout = false;
public static bool s_IsFoldoutOpen = false;
public static bool s_IsList = false;
public static bool s_TemporaryColor;
public static Color s_PreviousColor;
// If instantiating ourselves. Avoids reflection.
public PropertyAttribute _Attribute;
public FieldInfo _Field;
PropertyAttribute Attribute => _Attribute ?? attribute;
FieldInfo Field => _Field ?? fieldInfo;
public float Space { get; private set; }
UnityEditor.Editor _Editor;
Inspector _Inspector;
List<object> _Decorators = null;
List<object> Decorators
{
get
{
// Populate list with decorators.
_Decorators ??= fieldInfo
_Decorators ??= Field
.GetCustomAttributes(typeof(Decorator), false)
.ToList();
@@ -44,7 +55,7 @@ namespace WaveHarmonic.Crest.Editor
}
List<Attributes.Validator> _Validators = null;
List<Attributes.Validator> Validators => _Validators ??= fieldInfo
List<Attributes.Validator> Validators => _Validators ??= Field
.GetCustomAttributes(typeof(Attributes.Validator), false)
.Cast<Attributes.Validator>()
.ToList();
@@ -63,14 +74,39 @@ namespace WaveHarmonic.Crest.Editor
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var height = base.GetPropertyHeight(property, label);
if (property.isArray)
{
// Constructor caches value and this call retrieves it.
var list = ReorderableList.GetReorderableListFromSerializedProperty(property);
list ??= new ReorderableList(property.serializedObject, property);
// GetHeight does not include bottom buttons height.
height = property.isExpanded ? list.GetHeight() + list.footerHeight : height;
}
// Make original control rectangle be invisible because we always create our own. Zero still adds a little
// height which becomes noticeable once multiple properties are hidden. This could be some GUI style
// property but could not find which one.
return -2f;
return s_IsList ? height : -2f;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Get the owning editor.
if (_Editor == null)
{
foreach (var editor in ActiveEditorTracker.sharedTracker?.activeEditors)
{
if (editor.serializedObject == property.serializedObject)
{
_Editor = editor;
_Inspector = editor as Inspector;
break;
}
}
}
// Store the original GUI state so it can be reset later.
var originalColor = GUI.color;
var originalEnabled = GUI.enabled;
@@ -106,8 +142,8 @@ namespace WaveHarmonic.Crest.Editor
attribute.Decorate(position, property, label, this);
}
var a = (DecoratedProperty)attribute;
position = a.NeedsControlRectangle(property)
var a = (DecoratedProperty)Attribute;
position = a.NeedsControlRectangle(property) && (!s_IsList || property.isArray)
? EditorGUILayout.GetControlRect(true, EditorGUI.GetPropertyHeight(property, label, true))
: position;
@@ -120,11 +156,15 @@ namespace WaveHarmonic.Crest.Editor
label = attribute.BuildLabel(label);
}
var skipChange = fieldInfo.FieldType == typeof(UnityEvent);
var skipChange = Field.FieldType == typeof(UnityEvent) || property.isArray;
var isExpanded = property.isExpanded;
var isUndoRedo = false;
if (!skipChange)
{
EditorGUI.BeginChangeCheck();
isUndoRedo = _Inspector != null && _Inspector._UndoRedo;
}
var oldValue = skipChange ? null : property.boxedValue;
@@ -135,13 +175,19 @@ namespace WaveHarmonic.Crest.Editor
Validators[index].Validate(position, property, label, this, oldValue);
}
// Guard against foldouts triggering change check due to changing isExpanded.
if (!skipChange && EditorGUI.EndChangeCheck() && oldValue != property.boxedValue)
if (!skipChange && (EditorGUI.EndChangeCheck() || isUndoRedo))
{
// Apply any changes.
property.serializedObject.ApplyModifiedProperties();
if (!isUndoRedo)
{
property.serializedObject.ApplyModifiedProperties();
}
OnChange(property, oldValue);
// Guard against foldouts triggering change check due to changing isExpanded.
if (property.isExpanded == isExpanded)
{
OnChange(property, oldValue);
}
}
for (var index = 0; index < Decorators.Count; index++)
@@ -206,6 +252,7 @@ namespace WaveHarmonic.Crest.Editor
var relativePath = string.Join(".", chunks[(i + 1)..]);
var nestedTarget = nestedProperty.managedReferenceValue;
if (nestedTarget == null) continue;
var nestedTargetType = nestedTarget.GetType();
foreach (var (method, attribute) in OnChangeHandlers)

View File

@@ -294,7 +294,13 @@ namespace WaveHarmonic.Crest.Editor
width = Mathf.Max(width, minimumWidth);
// TODO: Add option to disable this (consistent width).
if (!hasDropDown && minimumWidth > 0) width += k_ButtonDropDownWidth;
if (centerLabel && hasDropDown) style.padding.left += k_ButtonDropDownWidth;
// TODO: eyeballed based on Fix button but likely specific to it.
if (centerLabel && hasDropDown)
{
style.padding.left += k_ButtonDropDownWidth / 2;
width += k_ButtonDropDownWidth / 3;
}
if (GUILayout.Button(label, style, expandWidth ? GUILayout.ExpandWidth(true) : GUILayout.Width(width)))
{

View File

@@ -9,6 +9,7 @@ using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Editor.Internal;
using WaveHarmonic.Crest.Internal;
using WaveHarmonic.Crest.Internal.Compatibility;
namespace WaveHarmonic.Crest.Editor
{
@@ -25,6 +26,7 @@ namespace WaveHarmonic.Crest.Editor
readonly Dictionary<FieldInfo, object> _MaterialOwners = new();
readonly Dictionary<Material, MaterialEditor> _MaterialEditors = new();
readonly Dictionary<string, DecoratedDrawer> _Lists = new();
public override bool RequiresConstantRepaint() => TexturePreview.s_ActiveInstance?.Open == true;
@@ -38,6 +40,9 @@ namespace WaveHarmonic.Crest.Editor
{
_MaterialOwners.Clear();
Undo.undoRedoPerformed -= OnUndoRedo;
Undo.undoRedoPerformed += OnUndoRedo;
foreach (var field in s_AttachMaterialEditors)
{
var target = (object)this.target;
@@ -74,12 +79,19 @@ namespace WaveHarmonic.Crest.Editor
protected virtual void OnDisable()
{
Undo.undoRedoPerformed -= OnUndoRedo;
foreach (var (_, editor) in _MaterialEditors)
{
Helpers.Destroy(editor);
}
}
protected virtual void OnChange()
{
}
protected virtual void RenderBeforeInspectorGUI()
{
if (this.target is EditorBehaviour target && target._IsPrefabStageInstance)
@@ -99,6 +111,8 @@ namespace WaveHarmonic.Crest.Editor
serializedObject.Update();
EditorGUI.BeginChangeCheck();
using var iterator = serializedObject.GetIterator();
if (iterator.NextVisible(true))
{
@@ -140,6 +154,33 @@ namespace WaveHarmonic.Crest.Editor
using (new EditorGUI.DisabledGroupScope(property.name == "m_Script"))
#endif
{
// Handle lists as PropertyDrawer is not called on the list itself.
if (property.isArray)
{
var field = property.GetFieldInfo(out var _);
var attribute = field?.GetCustomAttribute<Attributes.DecoratedProperty>();
if (field != null && attribute != null)
{
var id = GetPropertyIdentifier(property);
if (!_Lists.ContainsKey(id))
{
_Lists[id] = new DecoratedDrawer()
{
_Attribute = attribute,
_Field = field,
};
}
DecoratedDrawer.s_IsList = true;
var label = new GUIContent(property.displayName);
_Lists[id].OnGUI(Rect.zero, property, label);
DecoratedDrawer.s_IsList = false;
continue;
}
}
// Only support top level ordering for now.
EditorGUILayout.PropertyField(property, includeChildren: true);
}
@@ -148,11 +189,18 @@ namespace WaveHarmonic.Crest.Editor
// Need to call just in case there is no decorated property.
serializedObject.ApplyModifiedProperties();
if (EditorGUI.EndChangeCheck())
{
OnChange();
}
// Restore previous in case this is a nested editor.
Current = previous;
// Fixes indented validation etc.
EditorGUI.indentLevel = 0;
_UndoRedo = false;
}
protected virtual void RenderBottomButtons()
@@ -214,6 +262,45 @@ namespace WaveHarmonic.Crest.Editor
}
}
// Reflection
partial class Inspector
{
static readonly PropertyInfo s_GUIViewCurrent = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.GUIView").GetProperty("current", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
static readonly PropertyInfo s_GUIViewNativeHandle = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.GUIView").GetProperty("nativeHandle", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
// Adapted from:
// https://github.com/Unity-Technologies/UnityCsReference/blob/59b03b8a0f179c0b7e038178c90b6c80b340aa9f/Editor/Mono/Inspector/ReorderableListWrapper.cs#L77-L88
static string GetPropertyIdentifier(SerializedProperty serializedProperty)
{
// Property may be disposed.
try
{
var handle = -1;
var current = s_GUIViewCurrent.GetValue(null);
if (current != null)
{
handle = ((IntPtr)s_GUIViewNativeHandle.GetValue(current)).ToInt32();
}
return serializedProperty?.propertyPath + serializedProperty.serializedObject.targetObject.GetEntityId() + handle;
}
catch (NullReferenceException)
{
return string.Empty;
}
}
}
partial class Inspector
{
internal bool _UndoRedo;
void OnUndoRedo()
{
_UndoRedo = true;
}
}
// Adapted from:
// https://gist.github.com/thebeardphantom/1ad9aee0ef8de6271fff39f1a6a3d66d
static partial class Extensions

View File

@@ -0,0 +1,106 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor.Build;
using UnityEngine;
namespace WaveHarmonic.Crest.Editor.Reflected
{
static class BuildTargetGroup
{
public const int k_Server = -2;
}
static class BuildPlatform
{
internal static readonly Type s_BuildPlatformType = Type.GetType("UnityEditor.Build.BuildPlatform,UnityEditor.CoreModule");
internal static readonly Type s_BuildPlatformArrayType = s_BuildPlatformType.MakeArrayType();
static readonly FieldInfo s_NamedBuildTargetField = s_BuildPlatformType.GetField
(
"namedBuildTarget",
BindingFlags.Instance | BindingFlags.Public
);
public static NamedBuildTarget GetNamedBuildTarget(object platform)
{
return (NamedBuildTarget)s_NamedBuildTargetField.GetValue(platform);
}
}
static class BuildPlatforms
{
static readonly Type s_BuildPlatformsType = Type.GetType("UnityEditor.Build.BuildPlatforms,UnityEditor.CoreModule");
static readonly PropertyInfo s_BuildPlatformsInstanceProperty = s_BuildPlatformsType.GetProperty("instance", BindingFlags.Static | BindingFlags.Public);
static readonly MethodInfo s_GetValidPlatformsMethod = s_BuildPlatformsType.GetMethod("GetValidPlatforms", new Type[] { });
static Array s_Platforms; // Should be safe to cache.
public static Array GetValidPlatforms()
{
if (s_Platforms == null)
{
var instance = s_BuildPlatformsInstanceProperty.GetValue(null);
// We cannot just cast to the type we want it seems.
var enumerable = ((IEnumerable<object>)s_GetValidPlatformsMethod.Invoke(instance, null)).ToList();
s_Platforms = Array.CreateInstance(BuildPlatform.s_BuildPlatformType, enumerable.Count);
for (var i = 0; i < enumerable.Count; i++)
{
s_Platforms.SetValue(enumerable[i], i);
}
}
return s_Platforms;
}
}
static class EditorGUILayout
{
static readonly MethodInfo s_BeginPlatformGroupingMethod = typeof(UnityEditor.EditorGUILayout).GetMethod
(
"BeginPlatformGrouping",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new Type[]
{
BuildPlatform.s_BuildPlatformArrayType,
typeof(GUIContent),
},
null
);
static readonly object[] s_Parameters = new object[2];
public static int BeginBuildTargetSelectionGrouping(GUIContent defaultTab)
{
var platforms = BuildPlatforms.GetValidPlatforms();
s_Parameters[0] = platforms;
s_Parameters[1] = defaultTab;
var index = (int)s_BeginPlatformGroupingMethod.Invoke(null, s_Parameters);
if (index < 0)
{
// Default
return (int)UnityEditor.BuildTargetGroup.Unknown;
}
var target = BuildPlatform.GetNamedBuildTarget(platforms.GetValue(index));
if (target == NamedBuildTarget.Server)
{
// Server
return BuildTargetGroup.k_Server;
}
return (int)target.ToBuildTargetGroup();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cd362a01f52cc4002857e9f549c641be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -10,7 +10,7 @@ namespace WaveHarmonic.Crest.Editor.Settings
{
static class ScriptingSymbols
{
static NamedBuildTarget CurrentNamedBuildTarget
internal static NamedBuildTarget CurrentNamedBuildTarget
{
get
{

View File

@@ -10,6 +10,10 @@ namespace WaveHarmonic.Crest.Editor
{
abstract class TexturePreview : ObjectPreview
{
static readonly System.Reflection.MethodInfo s_DrawPreview = typeof(ObjectPreview).GetMethod("DrawPreview", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
static readonly object[] s_DrawPreviewArguments = new object[3];
static readonly Object[] s_DrawPreviewTargets = new Object[1];
public static TexturePreview s_ActiveInstance;
public bool Open { get; private set; }
@@ -101,6 +105,9 @@ namespace WaveHarmonic.Crest.Editor
Helpers.SafeCreateRenderTexture(ref _RenderTexture, descriptor);
_RenderTexture.Create();
Object.DestroyImmediate(_Editor);
// Raises both, but no way to avoid it:
// | The targets array should not be used inside OnSceneGUI or OnPreviewGUI. Use the single target property instead.
// | The serializedObject should not be used inside OnSceneGUI or OnPreviewGUI. Use the target property directly instead.
_Editor = UnityEditor.Editor.CreateEditor(_RenderTexture);
// Reset for incompatible copy.
descriptor = _OriginalDescriptor;
@@ -121,7 +128,13 @@ namespace WaveHarmonic.Crest.Editor
Graphics.CopyTexture(Texture, _RenderTexture);
}
_Editor.DrawPreview(rect);
s_DrawPreviewTargets[0] = _Editor.target;
s_DrawPreviewArguments[0] = _Editor;
s_DrawPreviewArguments[1] = rect;
s_DrawPreviewArguments[2] = s_DrawPreviewTargets;
// Use to be _Editor.DrawPreview(rect) but spammed errors with multiple selected.
s_DrawPreview?.Invoke(null, s_DrawPreviewArguments);
}
#if CREST_DEBUG

View File

@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Editor;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest.Editor
@@ -54,9 +55,9 @@ namespace WaveHarmonic.Crest.Editor
new(),
};
public delegate void ShowMessage(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null);
public delegate void ShowMessage(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null, Object caller = null);
public static void DebugLog(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null)
public static void DebugLog(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null, Object caller = null)
{
// Never log info validation to console.
if (type == MessageType.Info)
@@ -64,22 +65,27 @@ namespace WaveHarmonic.Crest.Editor
return;
}
message = $"Crest Validation: {message} {fixDescription} Click this message to highlight the problem object.";
// Always link back to the caller so developers know the origin. They can always
// use the help box "Inspect" once there to get to the object to fix. Even better,
// they can use any available fix buttons too.
var context = caller != null ? caller : @object;
message = $"<b>Crest Validation:</b> {message} {fixDescription} Click this message to highlight the problem object.";
switch (type)
{
case MessageType.Error: Debug.LogError(message, @object); break;
case MessageType.Warning: Debug.LogWarning(message, @object); break;
default: Debug.Log(message, @object); break;
case MessageType.Error: Debug.LogError(message, context); break;
case MessageType.Warning: Debug.LogWarning(message, context); break;
default: Debug.Log(message, context); break;
}
}
public static void HelpBox(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null)
public static void HelpBox(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null, Object caller = null)
{
s_Messages[(int)type].Add(new() { _Message = message, _FixDescription = fixDescription, _Object = @object, _Action = action, _PropertyPath = property });
}
public static void Suppressed(string _0, string _1, MessageType _2, Object _3 = null, FixValidation _4 = null, string _5 = null)
public static void Suppressed(string _0, string _1, MessageType _2, Object _3 = null, FixValidation _4 = null, string _5 = null, Object _6 = null)
{
}
@@ -291,6 +297,24 @@ namespace WaveHarmonic.Crest.Editor
}
}
// NOTE: Nested components do not descend from Object, but they could and this
// would work for them.
if (target is Object @object)
{
foreach (var field in TypeCache.GetFieldsWithAttribute<Validated>())
{
if (field.DeclaringType != type)
{
continue;
}
foreach (var attribute in field.GetCustomAttributes<Validated>())
{
isValid &= attribute.Validate(@object, field, messenger);
}
}
}
return isValid;
}
@@ -299,4 +323,89 @@ namespace WaveHarmonic.Crest.Editor
return ExecuteValidators(target, DebugLog);
}
}
abstract class Validated : System.Attribute
{
public abstract bool Validate(Object target, FieldInfo property, ValidatedHelper.ShowMessage messenger);
}
}
namespace WaveHarmonic.Crest
{
/// <summary>
/// Validates that field is not null.
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
sealed class Required : Validated
{
public override bool Validate(Object target, FieldInfo field, ValidatedHelper.ShowMessage messenger)
{
var isValid = true;
if ((Object)field.GetValue(target) == null)
{
var typeName = EditorHelpers.Pretty(target.GetType().Name);
var fieldName = EditorHelpers.Pretty(field.Name);
messenger
(
$"<i>{fieldName}</i> is required for the <i>{typeName}</i> component to function.",
$"Please set <i>{fieldName}</i>.",
ValidatedHelper.MessageType.Error,
target
);
isValid = false;
}
return isValid;
}
}
/// <summary>
/// Shows a info message if field is null.
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
sealed class Optional : Validated
{
readonly string _Message;
public Optional(string message)
{
_Message = message;
}
public override bool Validate(Object target, FieldInfo field, ValidatedHelper.ShowMessage messenger)
{
var value = field.GetValue(target);
if (value is ICollection<Object> list)
{
if (list != null && list.Count > 0)
{
return true;
}
}
else
{
if (value is Object @object && @object != null)
{
return true;
}
}
var typeName = EditorHelpers.Pretty(target.GetType().Name);
var fieldName = EditorHelpers.Pretty(field.Name);
messenger
(
$"<i>{fieldName}</i> is not set for the <i>{typeName}</i> component. " + _Message,
string.Empty,
ValidatedHelper.MessageType.Info,
target
);
return true;
}
}
}