using System.Collections; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace Sandbox.Physics; /// /// This is a JSON serializable description of the physics's collision rules. This allows us to send it /// to the engine - and store it in a string table (which is networked to the client). You shouldn't really /// ever have to mess with this, it's just used internally. /// [Expose] public class CollisionRules : ConfigData { /// /// Result of a collision between two objects. /// public enum Result { /// /// Fallback to default behavior. /// Unset, /// /// Collide. /// Collide, /// /// Do not collide, but trigger touch callbacks. /// Trigger, /// /// Do not collide. /// Ignore } public override int Version => 2; private record struct SerializedPair( [property: JsonPropertyName( "a" )] string Left, [property: JsonPropertyName( "b" )] string Right, [property: JsonPropertyName( "r" )] Result Result ); /// /// A pair of case- and order-insensitive tags, used as a key to look up a . /// public readonly struct Pair : IEquatable, IEnumerable { /// /// Initializes from a pair of tags. /// public static implicit operator Pair( (string Left, string Right) tuple ) { return new Pair( tuple.Left, tuple.Right ); } /// /// First of the two tags. /// public string Left { get; } /// /// Second of the two tags. /// public string Right { get; } /// /// Initializes from a pair of tags. /// public Pair( string left, string right ) { Left = left; Right = right; } /// /// Returns true if either or matches the given tag. /// public bool Contains( string tag ) { return string.Equals( Left, tag, StringComparison.OrdinalIgnoreCase ) || string.Equals( Right, tag, StringComparison.OrdinalIgnoreCase ); } /// public bool Equals( Pair other ) { return string.Equals( Left, other.Left, StringComparison.OrdinalIgnoreCase ) && string.Equals( Right, other.Right, StringComparison.OrdinalIgnoreCase ) || string.Equals( Left, other.Right, StringComparison.OrdinalIgnoreCase ) && string.Equals( Right, other.Left, StringComparison.OrdinalIgnoreCase ); } /// public override bool Equals( object obj ) { return obj is Pair other && Equals( other ); } /// public IEnumerator GetEnumerator() { yield return Left; yield return Right; } /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// public override int GetHashCode() { return StringComparer.OrdinalIgnoreCase.GetHashCode( Left ) + StringComparer.OrdinalIgnoreCase.GetHashCode( Right ); } public override string ToString() => $"{Left}, {Right}"; } public CollisionRules() { OnValidate(); } /// /// If no pair matching is found, this is what we'll use /// public Dictionary Defaults { get; set; } /// /// What happens when a pair collides /// [JsonIgnore] public Dictionary Pairs { get; set; } /// /// All tags with either an entry in or . /// [JsonIgnore] public IEnumerable Tags => Defaults.Keys.Union( Pairs.Keys.SelectMany( x => x ) ); /// /// Gets or sets in its serialized form for JSON. /// [JsonInclude, JsonPropertyName( "Pairs" )] public JsonNode SerializedPairs { get => JsonSerializer.SerializeToNode( Pairs?.Select( SerializePair ).ToArray(), Json.options ); private set { if ( value is null ) { Pairs = null; return; } var pairs = new Dictionary(); foreach ( var item in value.Deserialize( Json.options ) ) { if ( item.Result == Result.Unset ) { continue; } var key = new Pair( item.Left, item.Right ); // Pick least colliding of any duplicates pairs[key] = pairs.TryGetValue( key, out var existing ) ? LeastColliding( item.Result, existing ) : item.Result; } Pairs = pairs; } } private static SerializedPair SerializePair( KeyValuePair keyValue ) { return new SerializedPair( keyValue.Key.Left, keyValue.Key.Right, keyValue.Value ); } /// /// Selects the result with the highest precedence (least colliding). /// private static Result LeastColliding( Result a, Result b ) { return a >= b ? a : b; } /// /// Gets the specific collision rule for a pair of tags. /// public Result GetCollisionRule( string left, string right ) { var key = new Pair( left, right ); if ( !Pairs.TryGetValue( key, out var result ) ) { result = LeastColliding( Defaults.GetValueOrDefault( left ), Defaults.GetValueOrDefault( right ) ); } // If unset, collide return LeastColliding( result, Result.Collide ); } /// /// For each known tag, what result does it have when tested against the given ? /// internal IReadOnlyDictionary GetCollisionRules( string tag ) { return Tags.ToDictionary( x => x, x => GetCollisionRule( x, tag ), StringComparer.OrdinalIgnoreCase ); } /// /// For each known tag, what result does it have when tested against the given set of ? /// internal IReadOnlyDictionary GetCollisionRules( IEnumerable tags ) { return Tags.ToDictionary( x => x, x => { var result = Result.Collide; foreach ( var tag in tags ) { result = LeastColliding( result, GetCollisionRule( x, tag ) ); } return result; }, StringComparer.OrdinalIgnoreCase ); } /// /// Remove duplicates etc /// [Obsolete] public void Clean() { OnValidate(); } protected override void OnValidate() { Pairs ??= new() { [("solid", "solid")] = Result.Collide, [("trigger", "playerclip")] = Result.Ignore, [("trigger", "solid")] = Result.Trigger, [("playerclip", "solid")] = Result.Collide, }; Defaults ??= new() { ["solid"] = Result.Collide, ["world"] = Result.Collide, ["trigger"] = Result.Trigger, ["ladder"] = Result.Ignore, ["water"] = Result.Trigger, }; } public override int GetHashCode() { HashCode hc = default; foreach ( var (key, value) in Pairs ) { hc.Add( key.Left ); hc.Add( key.Right ); hc.Add( value ); } foreach ( var (key, value) in Defaults ) { hc.Add( key ); hc.Add( value ); } return hc.ToHashCode(); } // Upgrader to add the sound tag to existing projects that don't already have it [Expose, JsonUpgrader( typeof( CollisionRules ), 2 )] static void Upgrader_v2( JsonObject json ) { } }