Update EntityPath

- Add this[Range] indexer
- Add TryParse method
- Add GetParts method
- Fix not being able to handle entity IDs
wip/source-generators
copygirl 1 year ago
parent 9b0b44d1e9
commit 51aa3ee2fe
  1. 76
      src/gaemstone/ECS/EntityPath.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using gaemstone.Utility; using gaemstone.Utility;
@ -23,19 +24,23 @@ public class EntityPath
=> (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1]) => (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1])
: throw new ArgumentOutOfRangeException(nameof(index)); : 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) internal EntityPath(bool absolute, params byte[][] parts)
{ {
if (parts.Length == 0) throw new ArgumentException( if (parts.Length == 0) throw new ArgumentException(
"Must have at least one part", nameof(parts)); "Must have at least one part", nameof(parts));
IsAbsolute = absolute; IsAbsolute = absolute;
_parts = parts; _parts = parts;
} }
public EntityPath(params string[] parts) public EntityPath(params string[] parts)
: this(false, parts) { } : this(false, parts) { }
public EntityPath(bool absolute, params string[] parts) public EntityPath(bool absolute, params string[] parts)
: this(absolute, parts.Select(part => { : this(absolute, parts.Select(part => {
ValidateName(part); if (GetNameValidationError(part) is string error)
throw new ArgumentException(error);
var byteCount = Encoding.UTF8.GetByteCount(part); var byteCount = Encoding.UTF8.GetByteCount(part);
// Includes NUL character at the end of bytes. // Includes NUL character at the end of bytes.
var bytes = new byte[byteCount + 1]; var bytes = new byte[byteCount + 1];
@ -43,6 +48,33 @@ public class EntityPath
return bytes; return bytes;
}).ToArray()) { } }).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) public static EntityPath Parse(string str)
{ {
if (str.Length == 0) throw new ArgumentException( 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); 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( if (name.Length == 0) return "Must not be empty";
"Must not be empty");
// NOTE: This is a hopefully straightforward way to also prevent "." // NOTE: This is a hopefully straightforward way to also prevent "."
// and ".." to be part of paths which may access the file system. // and ".." to be part of paths which may access the file system.
if (name[0] == '.') throw new ArgumentException( if (name[0] == '.') return "Must not begin with a dot";
"Must not begin with a dot");
foreach (var chr in name) if (char.IsControl(chr)) 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)'_' }; // 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"); // 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() public override string ToString()
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
@ -106,9 +142,12 @@ public class EntityPath
else if (parent.IsNone) parent = new(ecs_get_scope(universe)); else if (parent.IsNone) parent = new(ecs_get_scope(universe));
foreach (var part in path) foreach (var part in path)
fixed (byte* ptr = part.AsSpan()) fixed (byte* ptr = part.AsSpan()) {
if ((parent = new(ecs_lookup_child(universe, parent, ptr))).IsNone) // 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 Entity.None;
}
return parent; return parent;
} }
@ -141,8 +180,17 @@ public static class EntityPathExtensions
var current = (Entity)entity; var current = (Entity)entity;
var parts = new List<byte[]>(32); var parts = new List<byte[]>(32);
do { parts.Add(ecs_get_name(entity.Universe, current).FlecsToBytes()!); } do {
while ((current = new(ecs_get_target(entity.Universe, current, EcsChildOf, 0))).IsSome); 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(); parts.Reverse();
return new(true, parts.ToArray()); return new(true, parts.ToArray());

Loading…
Cancel
Save