|
|
|
@ -1,6 +1,7 @@ |
|
|
|
|
using System; |
|
|
|
|
using System.Buffers; |
|
|
|
|
using System.Collections.Generic; |
|
|
|
|
using System.Diagnostics.CodeAnalysis; |
|
|
|
|
using System.Linq; |
|
|
|
|
using System.Text; |
|
|
|
|
using gaemstone.Utility; |
|
|
|
@ -23,19 +24,23 @@ public class EntityPath |
|
|
|
|
=> (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1]) |
|
|
|
|
: throw new ArgumentOutOfRangeException(nameof(index)); |
|
|
|
|
|
|
|
|
|
public EntityPath this[Range range] |
|
|
|
|
=> new(IsAbsolute && (range.GetOffsetAndLength(Count).Offset == 0), _parts[range]); |
|
|
|
|
|
|
|
|
|
internal EntityPath(bool absolute, params byte[][] parts) |
|
|
|
|
{ |
|
|
|
|
if (parts.Length == 0) throw new ArgumentException( |
|
|
|
|
"Must have at least one part", nameof(parts)); |
|
|
|
|
IsAbsolute = absolute; |
|
|
|
|
_parts = parts; |
|
|
|
|
_parts = parts; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public EntityPath(params string[] parts) |
|
|
|
|
: this(false, parts) { } |
|
|
|
|
public EntityPath(bool absolute, params string[] parts) |
|
|
|
|
: this(absolute, parts.Select(part => { |
|
|
|
|
ValidateName(part); |
|
|
|
|
if (GetNameValidationError(part) is string error) |
|
|
|
|
throw new ArgumentException(error); |
|
|
|
|
var byteCount = Encoding.UTF8.GetByteCount(part); |
|
|
|
|
// Includes NUL character at the end of bytes. |
|
|
|
|
var bytes = new byte[byteCount + 1]; |
|
|
|
@ -43,6 +48,33 @@ public class EntityPath |
|
|
|
|
return bytes; |
|
|
|
|
}).ToArray()) { } |
|
|
|
|
|
|
|
|
|
public static bool TryParse(string str, [NotNullWhen(true)] out EntityPath? result) |
|
|
|
|
{ |
|
|
|
|
result = null; |
|
|
|
|
if (str.Length == 0) return false; |
|
|
|
|
|
|
|
|
|
var strSpan = str.AsSpan(); |
|
|
|
|
var isAbsolute = (str[0] == '/'); |
|
|
|
|
if (isAbsolute) strSpan = strSpan[1..]; |
|
|
|
|
|
|
|
|
|
var numSeparators = 0; |
|
|
|
|
foreach (var chr in strSpan) if (chr == '/') numSeparators++; |
|
|
|
|
|
|
|
|
|
var index = 0; |
|
|
|
|
var parts = new byte[numSeparators + 1][]; |
|
|
|
|
foreach (var part in strSpan.Split('/')) { |
|
|
|
|
if (GetNameValidationError(part) != null) return false; |
|
|
|
|
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); |
|
|
|
|
parts[index++] = bytes; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
result = new(isAbsolute, parts); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public static EntityPath Parse(string str) |
|
|
|
|
{ |
|
|
|
|
if (str.Length == 0) throw new ArgumentException( |
|
|
|
@ -52,18 +84,15 @@ public class EntityPath |
|
|
|
|
return (parts[0].Length == 0) ? new(true, parts[1..]) : new(parts); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public static void ValidateName(string name) |
|
|
|
|
public static string? GetNameValidationError(ReadOnlySpan<char> name) |
|
|
|
|
{ |
|
|
|
|
if (name.Length == 0) throw new ArgumentException( |
|
|
|
|
"Must not be empty"); |
|
|
|
|
|
|
|
|
|
if (name.Length == 0) return "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 (name[0] == '.') throw new ArgumentException( |
|
|
|
|
"Must not begin with a dot"); |
|
|
|
|
|
|
|
|
|
if (name[0] == '.') return "Must not begin with a dot"; |
|
|
|
|
foreach (var chr in name) if (char.IsControl(chr)) |
|
|
|
|
throw new ArgumentException("Must not contain contol characters"); |
|
|
|
|
return "Must not contain contol characters"; |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' }; |
|
|
|
@ -77,6 +106,13 @@ public class EntityPath |
|
|
|
|
// throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character"); |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
public string[] GetParts() |
|
|
|
|
{ |
|
|
|
|
var result = new string[Count]; |
|
|
|
|
for (var i = 0; i < Count; i++) result[i] = this[i]; |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public override string ToString() |
|
|
|
|
{ |
|
|
|
|
var builder = new StringBuilder(); |
|
|
|
@ -106,9 +142,12 @@ public class EntityPath |
|
|
|
|
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) |
|
|
|
|
fixed (byte* ptr = part.AsSpan()) { |
|
|
|
|
// FIXME: This breaks when using large entity IDs. |
|
|
|
|
parent = new(ecs_lookup_child(universe, parent, ptr)); |
|
|
|
|
if (parent.IsNone || !ecs_is_alive(universe, parent)) |
|
|
|
|
return Entity.None; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return parent; |
|
|
|
|
} |
|
|
|
@ -141,8 +180,17 @@ public static class EntityPathExtensions |
|
|
|
|
var current = (Entity)entity; |
|
|
|
|
var parts = new List<byte[]>(32); |
|
|
|
|
|
|
|
|
|
do { parts.Add(ecs_get_name(entity.Universe, current).FlecsToBytes()!); } |
|
|
|
|
while ((current = new(ecs_get_target(entity.Universe, current, EcsChildOf, 0))).IsSome); |
|
|
|
|
do { |
|
|
|
|
var name = ecs_get_name(entity.Universe, current).FlecsToBytes(); |
|
|
|
|
if (name != null) parts.Add(name); |
|
|
|
|
else { |
|
|
|
|
// If name is not set, use the numeric ID, instead. |
|
|
|
|
var id = current.ID.ToString(); |
|
|
|
|
var bytes = new byte[Encoding.UTF8.GetByteCount(id) + 1]; |
|
|
|
|
Encoding.UTF8.GetBytes(id, bytes); |
|
|
|
|
parts.Add(bytes); |
|
|
|
|
} |
|
|
|
|
} while ((current = new(ecs_get_target(entity.Universe, current, EcsChildOf, 0))).IsSome); |
|
|
|
|
|
|
|
|
|
parts.Reverse(); |
|
|
|
|
return new(true, parts.ToArray()); |
|
|
|
|