From 51aa3ee2fe2327b30f7dcaadfb56d8892773fe9c Mon Sep 17 00:00:00 2001 From: copygirl Date: Thu, 22 Dec 2022 01:06:45 +0100 Subject: [PATCH] Update EntityPath - Add this[Range] indexer - Add TryParse method - Add GetParts method - Fix not being able to handle entity IDs --- src/gaemstone/ECS/EntityPath.cs | 76 +++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/gaemstone/ECS/EntityPath.cs b/src/gaemstone/ECS/EntityPath.cs index 54f9e32..48f7ce1 100644 --- a/src/gaemstone/ECS/EntityPath.cs +++ b/src/gaemstone/ECS/EntityPath.cs @@ -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 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(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());