|
|
|
using System;
|
|
|
|
using System.Buffers;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Text;
|
|
|
|
using gaemstone.Utility;
|
|
|
|
using static flecs_hub.flecs;
|
|
|
|
|
|
|
|
namespace gaemstone.ECS;
|
|
|
|
|
|
|
|
public class EntityPath
|
|
|
|
{
|
|
|
|
private readonly byte[][] _parts;
|
|
|
|
|
|
|
|
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]
|
|
|
|
=> (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;
|
|
|
|
}
|
|
|
|
|
|
|
|
public EntityPath(params string[] parts)
|
|
|
|
: this(false, parts) { }
|
|
|
|
public EntityPath(bool absolute, params string[] parts)
|
|
|
|
: this(absolute, parts.Select(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];
|
|
|
|
Encoding.UTF8.GetBytes(part, bytes);
|
|
|
|
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(
|
|
|
|
"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);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static string? GetNameValidationError(ReadOnlySpan<char> name)
|
|
|
|
{
|
|
|
|
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] == '.') return "Must not begin with a dot";
|
|
|
|
foreach (var chr in name) if (char.IsControl(chr))
|
|
|
|
return "Must not contain contol characters";
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
// }
|
|
|
|
|
|
|
|
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();
|
|
|
|
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
|
|
|
|
{
|
|
|
|
private readonly EntityPath _path;
|
|
|
|
private int index = -1;
|
|
|
|
public UTF8View Current => _path[index];
|
|
|
|
internal Enumerator(EntityPath path) => _path = path;
|
|
|
|
public bool MoveNext() => (++index < _path.Count);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static unsafe Entity Lookup(
|
|
|
|
World world, Entity parent, EntityPath path, bool throwOnNotFound)
|
|
|
|
{
|
|
|
|
var start = path.IsAbsolute ? Entity.None // If path is absolute, ignore parent and use root.
|
|
|
|
: parent.IsNone ? new(ecs_get_scope(world)) // If no parent is specified, use the current scope.
|
|
|
|
: parent; // Otherwise just use the specified parent.
|
|
|
|
|
|
|
|
var current = start;
|
|
|
|
foreach (var part in path)
|
|
|
|
fixed (byte* ptr = part.AsSpan()) {
|
|
|
|
current = new(ecs_lookup_child(world, current, ptr));
|
|
|
|
if (current.IsSome && ecs_is_alive(world, current)) continue;
|
|
|
|
if (!throwOnNotFound) return Entity.None;
|
|
|
|
|
|
|
|
var startStr = EntityRef.CreateOrNull(world, start)?.GetFullPath().ToString() ?? start.ToString();
|
|
|
|
throw new World.EntityNotFoundException(
|
|
|
|
(start == parent) ? $"Child entity of '{startStr}' at '{path}' not found"
|
|
|
|
: start.IsSome ? $"Entity at scope '{startStr}' at '{path}' not found"
|
|
|
|
: $"Entity at '{path}' not found"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return current;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary>
|
|
|
|
internal static unsafe Entity EnsureEntityExists(
|
|
|
|
World world, 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(world));
|
|
|
|
|
|
|
|
var skipLookup = parent.IsNone;
|
|
|
|
foreach (var part in path)
|
|
|
|
fixed (byte* ptr = part.AsSpan())
|
|
|
|
if (skipLookup || (parent = new(ecs_lookup_child(world, parent, ptr))).IsNone) {
|
|
|
|
var desc = new ecs_entity_desc_t { name = ptr, sep = CStringExtensions.ETX };
|
|
|
|
if (parent.IsSome) desc.add[0] = Identifier.Pair(world.ChildOf, parent);
|
|
|
|
parent = new(ecs_entity_init(world, &desc));
|
|
|
|
skipLookup = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static class EntityPathExtensions
|
|
|
|
{
|
|
|
|
public static unsafe EntityPath GetFullPath(this EntityRef entity)
|
|
|
|
{
|
|
|
|
var current = (Entity)entity;
|
|
|
|
var parts = new List<byte[]>(32);
|
|
|
|
|
|
|
|
do {
|
|
|
|
var name = ecs_get_name(entity.World, 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.World, current, EcsChildOf, 0))).IsSome);
|
|
|
|
|
|
|
|
parts.Reverse();
|
|
|
|
return new(true, parts.ToArray());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public readonly ref struct UTF8View
|
|
|
|
{
|
|
|
|
private readonly ReadOnlySpan<byte> _bytes;
|
|
|
|
public UTF8View(ReadOnlySpan<byte> bytes)
|
|
|
|
=> _bytes = bytes;
|
|
|
|
|
|
|
|
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 string(UTF8View view) => view.ToString();
|
|
|
|
|
|
|
|
public Enumerator GetEnumerator() => new(_bytes);
|
|
|
|
public ref struct Enumerator
|
|
|
|
{
|
|
|
|
private readonly ReadOnlySpan<byte> _bytes;
|
|
|
|
private int index = 0;
|
|
|
|
private Rune _current = default;
|
|
|
|
public Rune Current => _current;
|
|
|
|
|
|
|
|
internal Enumerator(ReadOnlySpan<byte> bytes)
|
|
|
|
=> _bytes = bytes;
|
|
|
|
|
|
|
|
public bool MoveNext()
|
|
|
|
{
|
|
|
|
if (index >= _bytes.Length) return false;
|
|
|
|
if (Rune.DecodeFromUtf8(_bytes[index..], out _current, out var consumed) != OperationStatus.Done)
|
|
|
|
throw new InvalidOperationException("Contains invalid UTF8");
|
|
|
|
index += consumed;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|