@ -1,136 +1,92 @@
using System ;
using System.Buffers ;
using System.Globalization ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using System.Threading ;
using gaemstone.Utility ;
using static flecs_hub . flecs ;
using static flecs_hub . flecs . Runtime ;
namespace gaemstone.ECS ;
public class EntityPath
{
public const int MaxDepth = 3 2 ;
public const char Separator = '/' ;
private readonly byte [ ] [ ] _ parts ;
internal const byte SeparatorAsByte = ( byte ) Separator ;
internal static readonly CString SeparatorAsCString = ( CString ) Separator . ToString ( ) ;
private static readonly ThreadLocal < ( int Start , int End ) [ ] > PartsCache
= new ( ( ) = > new ( int , int ) [ MaxDepth ] ) ;
private readonly byte [ ] _ bytes ;
private readonly ( int Start , int End ) [ ] _ parts ;
public int Depth = > _ parts . Length - 1 ;
public bool IsAbsolute = > _ bytes [ 0 ] = = SeparatorAsByte ;
public bool IsAbsolute { get ; }
public bool IsRelative = > ! IsAbsolute ;
public int Count = > _ parts . Length ;
public UTF8View Name = > this [ ^ 1 ] ;
public EntityPath ? Parent = > ( Count > 1 ) ? new ( IsAbsolute , _ parts [ . . ^ 1 ] ) : null ;
public UTF8View this [ int index ] { get {
if ( index < 0 | | index > Depth )
throw new ArgumentOutOfRangeException ( nameof ( index ) ) ;
var ( start , end ) = _ parts [ index ] ;
return new ( _ bytes . AsSpan ( ) [ start . . end ] ) ;
} }
public UTF8View this [ int index ]
= > ( index > = 0 & & index < Count ) ? new ( _ parts [ index ] . AsSpan ( ) [ . . ^ 1 ] )
: throw new ArgumentOutOfRangeException ( nameof ( index ) ) ;
public unsafe EntityPath ( byte * pointer )
: this ( ArrayFromPointer ( pointer ) ) { }
private static unsafe byte [ ] ArrayFromPointer ( byte * pointer )
internal EntityPath ( bool absolute , params byte [ ] [ ] parts )
{
var length = 0 ;
while ( true ) if ( pointer [ length + + ] = = 0 ) break ;
return new Span < byte > ( pointer , length ) . ToArray ( ) ;
if ( parts . Length = = 0 ) throw new ArgumentException (
"Must have at least one part" , nameof ( parts ) ) ;
IsAbsolute = absolute ;
_ parts = parts ;
}
// TODO: public EntityPath(EntityPath @base, params string[] parts) { }
public EntityPath ( params string [ ] parts )
: this ( ConcatParts ( false , parts ) ) { }
: this ( false , parts ) { }
public EntityPath ( bool absolute , params string [ ] parts )
: this ( ConcatParts ( absolute , parts ) ) { }
private static byte [ ] ConcatParts ( bool absolute , string [ ] parts )
{
// number of slashes + NUL
var totalBytes = ( parts . Length - 1 ) + 1 ;
// If absolute, and parts doesn't already start with a slash, increase length by 1.
var prependSlash = absolute & & parts [ 0 ] . Length > 0 & & parts [ 0 ] [ 0 ] ! = Separator ;
if ( prependSlash ) totalBytes + + ;
foreach ( var part in parts )
totalBytes + = Encoding . UTF8 . GetByteCount ( part ) ;
var bytes = new byte [ totalBytes ] ;
var index = 0 ;
foreach ( var part in parts ) {
if ( index > 0 | | prependSlash ) bytes [ index + + ] = SeparatorAsByte ;
index + = Encoding . UTF8 . GetBytes ( part , 0 , part . Length , bytes , index ) ;
}
// NUL byte at the end of bytes.
// bytes[index++] = 0;
: this ( absolute , parts . Select ( part = > {
ValidateName ( part ) ;
var byteCount = Encoding . UTF8 . GetByteCount ( part ) ;
// Includes NUL character at the end of bytes.
var bytes = new byte [ byteCount + 1 ] ;
Encoding . UTF8 . GetBytes ( part , bytes ) ;
return bytes ;
}
} ) . ToArray ( ) ) { }
private EntityPath ( byte [ ] bytes )
public static EntityPath Parse ( string str )
{
if ( bytes . Length < = 1 ) throw new ArgumentException ( "Must not be empty" ) ;
if ( bytes [ ^ 1 ] ! = 0 ) throw new ArgumentException ( "Must end with a NUL character" ) ;
_ bytes = bytes ;
var depth = 0 ;
var index = 0 ;
var partStart = 0 ;
var partsCache = PartsCache . Value ! ;
// If path starts with separator, it's an absolute path. Skip first byte.
if ( _ bytes [ 0 ] = = SeparatorAsByte ) index = partStart = 1 ;
// -1 is used here because we don't want to include the NUL character.
while ( index < _ bytes . Length - 1 ) {
if ( _ bytes [ index ] = = SeparatorAsByte ) {
// +1 is used here because one more part will follow after the loop.
if ( depth + 1 > = MaxDepth ) throw new ArgumentException (
$"Must not exceed maximum depth of {MaxDepth}" ) ;
partsCache [ depth + + ] = ( Start : partStart , End : index ) ;
ValidatePart ( _ bytes . AsSpan ( ) [ partStart . . index ] ) ;
partStart = + + index ;
} else {
var slice = _ bytes . AsSpan ( ) [ index . . ] ;
if ( Rune . DecodeFromUtf8 ( slice , out var rune , out var consumed ) ! = OperationStatus . Done )
throw new ArgumentException ( "Contains invalid UTF8" ) ;
ValidateRune ( rune ) ;
index + = consumed ;
}
}
partsCache [ depth ] = ( Start : partStart , End : index ) ;
ValidatePart ( _ bytes . AsSpan ( ) [ partStart . . ^ 1 ] ) ;
// Copy parts from the thread local cache - this avoids unnecessary resizing.
_ parts = partsCache [ . . ( depth + 1 ) ] ;
if ( str . Length = = 0 ) throw new ArgumentException (
"String must not be empty" , nameof ( str ) ) ;
var parts = str . Split ( '/' ) ;
// If string starts with a slash, first part will be empty, so create an absolute path.
return ( parts [ 0 ] . Length = = 0 ) ? new ( true , parts [ 1. . ] ) : new ( parts ) ;
}
private static void ValidatePart ( ReadOnlySpan < byte > part )
public static void ValidateName ( string name )
{
if ( part . Length = = 0 ) throw new ArgumentException (
"Must not contain empty parts" ) ;
if ( name . Length = = 0 ) throw new ArgumentException (
"Must not be empty" ) ;
// NOTE: This is a hopefully straightforward way to also prevent "."
// and ".." to be part of paths which may access the file system.
if ( part [ 0 ] = = ( byte ) '.' ) throw new ArgumentException (
"Must not contain parts that start with a dot" ) ;
if ( name [ 0 ] = = '.' ) throw new ArgumentException (
"Must not begin with a dot" ) ;
foreach ( var chr in name ) if ( char . IsControl ( chr ) )
throw new ArgumentException ( "Must not contain contol characters" ) ;
}
private static readonly Rune [ ] _ validRunes = { ( Rune ) '-' , ( Rune ) '.' , ( Rune ) '_' } ;
private static readonly UnicodeCategory [ ] _ validCategories = {
UnicodeCategory . LowercaseLetter , UnicodeCategory . UppercaseLetter ,
UnicodeCategory . OtherLetter , UnicodeCategory . DecimalDigitNumber } ;
// private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' };
// private static readonly UnicodeCategory[] _validCategories = {
// UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter,
// UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber };
// private static void ValidateRune(Rune rune)
// {
// if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune)))
// throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character");
// }
private static void ValidateRune ( Rune rune )
public override string ToString ( )
{
if ( ! _ validRunes . Contains ( rune ) & & ! _ validCategories . Contains ( Rune . GetUnicodeCategory ( rune ) ) )
throw new ArgumentException ( $"Must not contain {Rune.GetUnicodeCategory(rune)} character" ) ;
var builder = new StringBuilder ( ) ;
if ( IsAbsolute ) builder . Append ( '/' ) ;
foreach ( var part in this ) builder . Append ( part ) . Append ( '/' ) ;
return builder . ToString ( 0 , builder . Length - 1 ) ;
}
public static implicit operator EntityPath ( string path ) = > Parse ( path ) ;
public Enumerator GetEnumerator ( ) = > new ( this ) ;
public ref struct Enumerator
{
@ -138,32 +94,58 @@ public class EntityPath
private int index = - 1 ;
public UTF8View Current = > _ path [ index ] ;
internal Enumerator ( EntityPath path ) = > _ path = path ;
public bool MoveNext ( ) = > ( + + index > = _ path . Depth ) ;
public bool MoveNext ( ) = > ( + + index < _ path . Count ) ;
}
internal static unsafe Entity Lookup ( Universe universe , Entity parent , EntityPath path )
{
// If path is absolute, ignore parent and start at root.
if ( path . IsAbsolute ) parent = default ;
// Otherwise, if no parent is specified, use the current scope.
else if ( parent . IsNone ) parent = new ( ecs_get_scope ( universe ) ) ;
foreach ( var part in path )
fixed ( byte * ptr = part . AsSpan ( ) )
if ( ( parent = new ( ecs_lookup_child ( universe , parent , ptr ) ) ) . IsNone )
return Entity . None ;
return parent ;
}
public ReadOnlySpan < byte > AsSpan ( bool includeNul = false )
= > includeNul ? _ bytes . AsSpan ( ) : _ bytes . AsSpan ( ) [ . . ^ 1 ] ;
/// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary>
internal static unsafe Entity EnsureEntityExists (
Universe universe , Entity parent , EntityPath path )
{
// If no parent is specified and path is relative, use the current scope.
if ( parent . IsNone & & path . IsRelative ) parent = new ( ecs_get_scope ( universe ) ) ;
var skipLookup = parent . IsNone ;
foreach ( var part in path )
fixed ( byte * ptr = part . AsSpan ( ) )
if ( skipLookup | | ( parent = new ( ecs_lookup_child ( universe , parent , ptr ) ) ) . IsNone ) {
var desc = new ecs_entity_desc_t { name = ptr } ;
if ( parent . IsSome ) desc . add [ 0 ] = Identifier . Pair ( universe . ChildOf , parent ) ;
parent = new ( ecs_entity_init ( universe , & desc ) ) ;
skipLookup = true ;
}
public override string ToString ( ) = > Encoding . UTF8 . GetString ( AsSpan ( ) ) ;
public static implicit operator string ( EntityPath view ) = > view . ToString ( ) ;
public static implicit operator EntityPath ( string str ) = > new ( str ) ;
return parent ;
}
}
public static class EntityPathExtensions
{
public static unsafe EntityPath GetFullPath ( this EntityRef entity )
{
var cStr = ecs_get_path_w_sep ( entity . Universe , default , entity ,
EntityPath . SeparatorAsCString , EntityPath . SeparatorAsCString ) ;
try { return new ( ( byte * ) ( nint ) cStr ) ; }
finally { cStr . FlecsFree ( ) ; }
}
var current = ( Entity ) entity ;
var parts = new List < byte [ ] > ( 3 2 ) ;
public static unsafe Entity Lookup ( Universe universe , Entity parent , EntityPath path )
{
using var alloc = TempAllocator . Lock ( ) ;
return new ( ecs_lookup_path_w_sep ( universe , parent , alloc . AllocateCString ( path . AsSpan ( true ) ) ,
EntityPath . SeparatorAsCString , EntityPath . SeparatorAsCString , true ) ) ;
do { parts . Add ( ecs_get_name ( entity . Universe , current ) . FlecsToBytes ( ) ! ) ; }
while ( ( current = new ( ecs_get_target ( entity . Universe , current , EcsChildOf , 0 ) ) ) . IsSome ) ;
parts . Reverse ( ) ;
return new ( true , parts . ToArray ( ) ) ;
}
}
@ -176,8 +158,8 @@ public readonly ref struct UTF8View
public int Length = > _ bytes . Length ;
public byte this [ int index ] = > _ bytes [ index ] ;
public ReadOnlySpan < byte > AsSpan ( ) = > _ bytes ;
public override string ToString ( ) = > Encoding . UTF8 . GetString ( _ bytes ) ;
public static implicit operator ReadOnlySpan < byte > ( UTF8View view ) = > view . _ bytes ;
public static implicit operator string ( UTF8View view ) = > view . ToString ( ) ;
public Enumerator GetEnumerator ( ) = > new ( _ bytes ) ;