mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-20 04:10:00 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
697 lines
16 KiB
C#
697 lines
16 KiB
C#
using Editor.NodeEditor;
|
|
using Facepunch.ActionGraphs;
|
|
using Sandbox;
|
|
using Sandbox.Internal;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Editor.ActionGraphs;
|
|
|
|
public class Properties : Widget
|
|
{
|
|
|
|
private static HashSet<Type> DefaultNumericTypes { get; } = new()
|
|
{
|
|
typeof(int),
|
|
typeof(float),
|
|
typeof(Vector2),
|
|
typeof(Vector3),
|
|
typeof(Vector4)
|
|
};
|
|
|
|
private static HashSet<Type> DefaultTypes { get; } = new()
|
|
{
|
|
typeof(int),
|
|
typeof(float),
|
|
typeof(Vector2),
|
|
typeof(Vector3),
|
|
typeof(Vector4),
|
|
typeof(Color),
|
|
typeof(string),
|
|
typeof(bool),
|
|
typeof(object)
|
|
};
|
|
|
|
public static Type DefaultType { get; } = Either.CreateType( DefaultTypes );
|
|
|
|
public static Type DefaultNumericType { get; } = Either.CreateType( DefaultNumericTypes );
|
|
|
|
public static Type PredictBestType( Node.IParameter target )
|
|
{
|
|
if ( target is not Node.Input )
|
|
{
|
|
return DefaultType;
|
|
}
|
|
|
|
if ( !target.Node.Definition.IsOperator() )
|
|
{
|
|
return DefaultType;
|
|
}
|
|
|
|
var otherInput = target.Name switch
|
|
{
|
|
"a" => target.Node.Inputs.TryGetValue( "b", out var b ) ? b : null,
|
|
"b" => target.Node.Inputs.TryGetValue( "a", out var a ) ? a : null,
|
|
_ => null
|
|
};
|
|
|
|
switch ( target.Node.Definition.Identifier )
|
|
{
|
|
case "op.conditional":
|
|
case "op.coalesce":
|
|
case "op.equal":
|
|
case "op.notequal":
|
|
return otherInput?.SourceType ?? DefaultType;
|
|
|
|
default:
|
|
return otherInput?.SourceType is { } otherType ? typeof( Either<,> ).MakeGenericType( otherType, DefaultNumericType ) : DefaultNumericType;
|
|
}
|
|
}
|
|
|
|
private record PropertyRow( SerializedProperty Property, ControlWidget Widget, Node.IParameter Parameter = null, int? Index = null );
|
|
|
|
private SerializedProperty _lastActive;
|
|
private object _target;
|
|
private readonly List<PropertyRow> _rows = new();
|
|
Layout Content;
|
|
|
|
private ControlWidget _startEditingTarget;
|
|
|
|
public MainWindow MainWindow { get; }
|
|
|
|
public object Target
|
|
{
|
|
get => _target;
|
|
set
|
|
{
|
|
if ( _target == value )
|
|
{
|
|
return;
|
|
}
|
|
|
|
_target = value;
|
|
_contentInvalid = true;
|
|
}
|
|
}
|
|
|
|
private bool _contentInvalid;
|
|
|
|
public Properties( MainWindow mainWindow ) : base( null )
|
|
{
|
|
MainWindow = mainWindow;
|
|
|
|
Name = "Inspector";
|
|
WindowTitle = "Inspector";
|
|
SetWindowIcon( "manage_search" );
|
|
|
|
Layout = Layout.Column();
|
|
Content = Layout.AddColumn();
|
|
|
|
SetSizeMode( SizeMode.Default, SizeMode.CanShrink );
|
|
|
|
MainWindow.SelectionChanged += OnSelectionChanged;
|
|
MainWindow.FocusedOnInput += FocusOnInput;
|
|
}
|
|
|
|
public override void OnDestroyed()
|
|
{
|
|
base.OnDestroyed();
|
|
|
|
MainWindow.SelectionChanged -= OnSelectionChanged;
|
|
MainWindow.FocusedOnInput -= FocusOnInput;
|
|
}
|
|
|
|
private void OnSelectionChanged( object target )
|
|
{
|
|
Target = target;
|
|
}
|
|
|
|
[EditorEvent.Frame]
|
|
public void Frame()
|
|
{
|
|
var target = Target;
|
|
|
|
if ( target is NodeUI nodeUi )
|
|
{
|
|
target = nodeUi.Node;
|
|
}
|
|
|
|
if ( target is EditorNode node )
|
|
{
|
|
if ( !node.Node.IsValid )
|
|
{
|
|
Target = null;
|
|
}
|
|
else
|
|
{
|
|
node.MarkDirty();
|
|
}
|
|
}
|
|
|
|
if ( _contentInvalid )
|
|
{
|
|
_contentInvalid = false;
|
|
RebuildContent();
|
|
}
|
|
|
|
if ( _startEditingTarget.IsValid() )
|
|
{
|
|
_startEditingTarget.StartEditing();
|
|
_startEditingTarget = null;
|
|
}
|
|
|
|
SerializedProperty active = null;
|
|
|
|
foreach ( var row in _rows )
|
|
{
|
|
if ( row.Widget.IsControlActive )
|
|
{
|
|
active = row.Property;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( _lastActive != active )
|
|
{
|
|
_lastActive = active;
|
|
|
|
if ( active != null )
|
|
{
|
|
MainWindow.FocusedView?.PushUndo( $"Set Property ({active?.Name})" );
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnFocus( FocusChangeReason reason )
|
|
{
|
|
MainWindow.FocusedView?.PushUndo( "Edit Properties" );
|
|
}
|
|
|
|
public void FocusOnInput( Node.Input input, int? index )
|
|
{
|
|
if ( _contentInvalid )
|
|
{
|
|
_contentInvalid = false;
|
|
RebuildContent();
|
|
}
|
|
|
|
var match = _rows.FirstOrDefault( x => x.Parameter == input && x.Index == index )
|
|
?? _rows.FirstOrDefault( x => x.Parameter == input && x.Index == null );
|
|
|
|
_startEditingTarget = match?.Widget;
|
|
}
|
|
|
|
void RebuildContent()
|
|
{
|
|
_lastActive = null;
|
|
|
|
Content.Clear( true );
|
|
_rows.Clear();
|
|
|
|
if ( _target == null )
|
|
{
|
|
return;
|
|
}
|
|
|
|
var ps = new ControlSheet();
|
|
var target = Target;
|
|
|
|
Content.Add( ps );
|
|
Content.AddStretchCell();
|
|
|
|
if ( target is NodeUI nodeUi )
|
|
{
|
|
target = nodeUi.Node;
|
|
}
|
|
else
|
|
{
|
|
nodeUi = null;
|
|
}
|
|
|
|
if ( target is CommentEditorNode commentNode )
|
|
{
|
|
var obj = AddObject( ps, EditorTypeLibrary.GetSerializedObject( commentNode ) );
|
|
obj.OnPropertyChanged += _ => commentNode.MarkDirty();
|
|
|
|
}
|
|
else if ( target is EditorActionGraph actionGraph )
|
|
{
|
|
var obj = AddObject( ps, EditorTypeLibrary.GetSerializedObject( actionGraph ) );
|
|
var inputNode = actionGraph.FindNode( actionGraph.Graph.InputNode );
|
|
|
|
obj.OnPropertyChanged += _ => inputNode?.MarkDirty();
|
|
}
|
|
else if ( target is EditorNode node )
|
|
{
|
|
var obj = AddObject( ps, EditorTypeLibrary.GetSerializedObject( node ) );
|
|
obj.OnPropertyChanged += _ => nodeUi!.Position = node.Position;
|
|
|
|
obj = AddObject( ps, new SerializedActionNode( this, node ) );
|
|
obj.OnPropertyChanged += _ => node.MarkDirty();
|
|
}
|
|
}
|
|
|
|
private SerializedObject AddObject( ControlSheet ps, SerializedObject obj )
|
|
{
|
|
ps.AddObject( obj );
|
|
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
internal class SerializedActionNode : SerializedObject
|
|
{
|
|
private static bool CanEditInputType( Type type )
|
|
{
|
|
if ( type is null ) return false;
|
|
if ( Either.IsEitherType( type ) ) return true;
|
|
|
|
if ( type.IsAbstract || type.IsInterface || type.IsArray || type.ContainsGenericParameters )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( type == typeof( Task ) || type == typeof( Task[] ) ) return false;
|
|
if ( type == typeof( Variable ) ) return false;
|
|
|
|
// Should use scene ref / resource ref nodes instead
|
|
if ( type == typeof( GameObject ) ) return false;
|
|
if ( type.IsAssignableTo( typeof( Component ) ) ) return false;
|
|
if ( type.IsAssignableTo( typeof( Resource ) ) ) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private static HashSet<string> HiddenProperties { get; } = new()
|
|
{
|
|
"graph/graph",
|
|
"input.value/_name",
|
|
"var/_var",
|
|
"property/_name", "property/_type",
|
|
"call/_name", "call/_type", "call/_isStatic",
|
|
"new/_type",
|
|
"switch/cases"
|
|
};
|
|
|
|
private static HashSet<string> HiddenInputs { get; } = new()
|
|
{
|
|
|
|
};
|
|
|
|
public SerializedActionNode( Properties properties, EditorNode node )
|
|
{
|
|
PropertyList = new();
|
|
|
|
foreach ( var (name, property) in node.Node.Properties )
|
|
{
|
|
if ( property.Display.Hidden is true )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( HiddenProperties.Contains( $"{node.Definition.Identifier}/{name}" ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PropertyList.Add( new SerializedNodeProperty( properties, node, property ) );
|
|
}
|
|
|
|
if ( node.Definition.Identifier == "switch" )
|
|
{
|
|
AddSwitchNodeProperties( properties, node );
|
|
}
|
|
|
|
foreach ( var (name, input) in node.Node.Inputs )
|
|
{
|
|
if ( HiddenInputs.Contains( $"{node.Definition.Identifier}/{name}" ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !input.IsArray )
|
|
{
|
|
if ( input.IsLinked && input.Link?.TryGetConstant( out _ ) is not true )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !CanEditInputType( input.Type ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PropertyList.Add( new SerializedNodeInput( properties, node, input ) );
|
|
}
|
|
else
|
|
{
|
|
if ( input.Link is not null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !CanEditInputType( input.ElementType ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var count = input.LinkArray?.Count ?? 0;
|
|
|
|
for ( var i = 0; i <= count; ++i )
|
|
{
|
|
if ( i < count && input.LinkArray?[i].TryGetConstant( out _ ) is not true )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PropertyList.Add( new SerializedNodeInputElement( properties, node, input, i ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddSwitchNodeProperties( Properties properties, EditorNode node )
|
|
{
|
|
var valueType = GetSwitchValueType( node.Node ) ?? typeof( string );
|
|
var sPropType = typeof( SerializedSwitchCasesProperty<> ).MakeGenericType( valueType );
|
|
|
|
PropertyList.Add( (SerializedProperty)Activator.CreateInstance( sPropType, properties, node ) );
|
|
}
|
|
|
|
private Type GetSwitchValueType( Node node )
|
|
{
|
|
if ( node.Inputs["value"].SourceType is { } valueType )
|
|
{
|
|
return valueType;
|
|
}
|
|
|
|
if ( node.Properties["cases"].Value is not { } casesObj )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach ( var iFace in casesObj.GetType().GetInterfaces() )
|
|
{
|
|
if ( !iFace.IsConstructedGenericType ) continue;
|
|
if ( iFace.GetGenericTypeDefinition() != typeof( IReadOnlyList<> ) ) continue;
|
|
|
|
return iFace.GetGenericArguments()[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public interface ISerializedNodeParameter
|
|
{
|
|
Node.IParameter Parameter { get; }
|
|
}
|
|
|
|
internal abstract class SerializedNodeParameter<T> : SerializedProperty, ISerializedNodeParameter
|
|
where T : Node.IParameter
|
|
{
|
|
public Properties Properties { get; }
|
|
public EditorNode Node { get; }
|
|
public T Target { get; }
|
|
|
|
public override string Name => Target.Name;
|
|
public override string DisplayName => Target.Display.Title;
|
|
public override string Description => Target.Display.Description;
|
|
public override Type PropertyType => Target.Type == typeof( object )
|
|
? Properties.PredictBestType( Target )
|
|
: Nullable.GetUnderlyingType( Target.Type ) ?? Target.Type;
|
|
|
|
public abstract object Value { get; set; }
|
|
public abstract object DefaultValue { get; }
|
|
public abstract bool IsRequired { get; }
|
|
public abstract bool HasValue { get; }
|
|
|
|
Node.IParameter ISerializedNodeParameter.Parameter => Target;
|
|
|
|
public override IEnumerable<Attribute> GetAttributes() => Target.Attributes.Union( new[] { new AllowNullAttribute() } );
|
|
|
|
protected SerializedNodeParameter( Properties properties, EditorNode node, T target )
|
|
{
|
|
Properties = properties;
|
|
Node = node;
|
|
Target = target;
|
|
}
|
|
|
|
public override bool TryGetAsObject( out SerializedObject obj )
|
|
{
|
|
obj = null;
|
|
|
|
var description = EditorTypeLibrary.GetType( PropertyType );
|
|
|
|
if ( description == null )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
if ( !PropertyType.IsValueType )
|
|
{
|
|
var curVal = GetValue<object>();
|
|
|
|
if ( curVal == null )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
obj = EditorTypeLibrary.GetSerializedObject( curVal );
|
|
return true;
|
|
}
|
|
|
|
obj = EditorTypeLibrary.GetSerializedObject( () => HasValue && Value is not null ? Value : IsRequired || DefaultValue is null
|
|
? Activator.CreateInstance( PropertyType )
|
|
: DefaultValue,
|
|
description, this );
|
|
return true;
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Warning( e );
|
|
obj = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static object ConvertTo( object value, Type sourceType, Type targetType )
|
|
{
|
|
if ( sourceType == targetType || sourceType.IsAssignableTo( targetType ) )
|
|
{
|
|
return value;
|
|
}
|
|
|
|
if ( Nullable.GetUnderlyingType( targetType ) is { } underlyingTargetType && Nullable.GetUnderlyingType( sourceType ) is null )
|
|
{
|
|
if ( value is null )
|
|
{
|
|
return Activator.CreateInstance( targetType, null );
|
|
}
|
|
|
|
return Activator.CreateInstance( targetType, ConvertTo( value, sourceType, underlyingTargetType ) );
|
|
}
|
|
|
|
if ( sourceType == typeof( long ) && targetType.IsEnum )
|
|
{
|
|
// Special case for EnumControlWidget :S
|
|
|
|
return Enum.ToObject( targetType, (long)value );
|
|
}
|
|
|
|
if ( sourceType.IsEnum && targetType == typeof( long ) )
|
|
{
|
|
// Special case for EnumControlWidget :S
|
|
|
|
return Convert.ChangeType(
|
|
Convert.ChangeType(
|
|
value,
|
|
Enum.GetUnderlyingType( sourceType ) ),
|
|
targetType );
|
|
}
|
|
|
|
return Convert.ChangeType( value, targetType );
|
|
}
|
|
|
|
public override void SetValue<TVal>( TVal value )
|
|
{
|
|
if ( Either.IsEitherType( PropertyType ) )
|
|
{
|
|
Value = value;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
Value = ConvertTo( value, typeof( TVal ), PropertyType );
|
|
}
|
|
catch
|
|
{
|
|
Value = value;
|
|
}
|
|
}
|
|
|
|
Node.MarkDirty();
|
|
}
|
|
|
|
public override TVal GetValue<TVal>( TVal defaultValue = default )
|
|
{
|
|
var rawValue = HasValue ? Value : Target.Definition.IsRequired ? defaultValue : DefaultValue;
|
|
|
|
if ( rawValue is TVal value )
|
|
{
|
|
return value;
|
|
}
|
|
|
|
if ( rawValue is null )
|
|
{
|
|
return defaultValue;
|
|
}
|
|
|
|
try
|
|
{
|
|
return (TVal)ConvertTo( rawValue, rawValue.GetType(), typeof( TVal ) );
|
|
}
|
|
catch
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class SerializedNodeProperty : SerializedNodeParameter<Node.Property>
|
|
{
|
|
public override string GroupName => "Properties";
|
|
|
|
public SerializedNodeProperty( Properties properties, EditorNode node, Node.Property target ) : base( properties, node, target ) { }
|
|
|
|
public override object Value
|
|
{
|
|
get => Target.Value;
|
|
set => Target.Value = value;
|
|
}
|
|
|
|
public override Type PropertyType => Target.Type;
|
|
public override object DefaultValue => Target.Definition.IsRequired ? null : Target.Definition.Default;
|
|
|
|
public override bool HasValue => Target.Value is not null;
|
|
public override bool IsRequired => Target.Definition.IsRequired;
|
|
}
|
|
|
|
internal class SerializedSwitchCasesProperty<T> : SerializedNodeProperty
|
|
{
|
|
public override Type PropertyType { get; }
|
|
|
|
private List<T> _value;
|
|
|
|
public override object Value
|
|
{
|
|
get
|
|
{
|
|
if ( _value is not null ) return _value;
|
|
|
|
if ( base.Value is not IReadOnlyList<T> baseValue )
|
|
{
|
|
return _value = new List<T>( (IReadOnlyList<T>)base.DefaultValue );
|
|
}
|
|
|
|
if ( baseValue is List<T> list )
|
|
{
|
|
return _value = list;
|
|
}
|
|
|
|
return _value = new List<T>( baseValue );
|
|
}
|
|
set
|
|
{
|
|
_value = (List<T>)value;
|
|
base.Value = _value.ToArray();
|
|
}
|
|
}
|
|
|
|
public override bool HasValue => true;
|
|
|
|
public SerializedSwitchCasesProperty( Properties properties, EditorNode node )
|
|
: base( properties, node, node.Node.Properties["cases"] )
|
|
{
|
|
PropertyType = typeof( List<> ).MakeGenericType( typeof( T ) );
|
|
}
|
|
|
|
public override bool TryGetAsObject( out SerializedObject obj )
|
|
{
|
|
obj = EditorTypeLibrary.GetSerializedObject( Value );
|
|
obj.ParentProperty = this;
|
|
obj.OnPropertyChanged += prop =>
|
|
{
|
|
Node.Node.Properties["cases"].Value = null;
|
|
Node.Node.Properties["cases"].Value = ((List<T>)Value).ToArray();
|
|
Node.MarkDirty();
|
|
};
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
internal class SerializedNodeInput : SerializedNodeParameter<Node.Input>
|
|
{
|
|
public override string GroupName => "Inputs";
|
|
|
|
public SerializedNodeInput( Properties properties, EditorNode node, Node.Input target ) : base( properties, node, target ) { }
|
|
|
|
public override object Value
|
|
{
|
|
get => Target.Value;
|
|
set => Target.Value = value;
|
|
}
|
|
|
|
public override object DefaultValue => Target.Definition.IsRequired ? null : Target.Definition.Default;
|
|
|
|
public override bool HasValue => Target.Link?.TryGetConstant( out _ ) is true;
|
|
public override bool IsRequired => Target.Definition.IsRequired;
|
|
}
|
|
|
|
internal class SerializedNodeInputElement : SerializedNodeParameter<Node.Input>
|
|
{
|
|
public int Index { get; }
|
|
|
|
public override string DisplayName => $"{Target.Display.Title}[{Index}]";
|
|
public override string GroupName => "Inputs";
|
|
|
|
public SerializedNodeInputElement( Properties properties, EditorNode node, Node.Input target, int index ) : base( properties, node, target )
|
|
{
|
|
Index = index;
|
|
}
|
|
|
|
public override object Value
|
|
{
|
|
get => Target.LinkArray is { } linkArray && Index >= 0 && Index < linkArray.Count
|
|
&& linkArray[Index].TryGetConstant( out var value ) ? value : null;
|
|
set
|
|
{
|
|
if ( Target.LinkArray is { } linkArray && Index >= 0 && Index < linkArray.Count )
|
|
{
|
|
Target.SetLink( new Constant( value ), Index );
|
|
}
|
|
else
|
|
{
|
|
Target.InsertLink( new Constant( value ), Index );
|
|
}
|
|
}
|
|
}
|
|
|
|
public override Type PropertyType => Target.ElementType == typeof( object ) ? Properties.PredictBestType( Target ) : Target.ElementType;
|
|
public override object DefaultValue => Target.ElementType.IsValueType
|
|
? Activator.CreateInstance( Target.ElementType )
|
|
: null;
|
|
|
|
public override bool HasValue => Target.LinkArray is { } linkArray && Index >= 0 && Index < linkArray.Count
|
|
&& linkArray[Index].TryGetConstant( out _ );
|
|
public override bool IsRequired => false;
|
|
}
|