using System.Numerics;
using Engine.Kernel.Scheduling;
namespace Engine.Kernel.World;
///
/// The entity type: identity, hierarchy, and a list of components. See
/// docs/kernel-contract.md §2.
///
/// Construction is internal — the only way to get one is
/// , so can keep
/// its type index consistent instead of trusting callers to report changes.
///
public sealed class GameObject
{
private readonly List _components = [];
private readonly List _children = [];
internal GameObject(string name)
{
Name = name;
}
public string Name { get; set; }
public Transform Transform;
public GameObject? Parent { get; private set; }
public IReadOnlyList Children => _children;
public IReadOnlyList Components => _components;
///
/// Set by at creation and cleared on destroy.
/// Null means "not attached to a World" — a defensive state that
/// should never be observable from a plugin.
///
internal GameWorld? Owner { get; set; }
///
/// Composed from the parent chain on every read, not cached — a cache
/// here would need invalidating on every reparent and every ancestor's
/// transform change, which is more bookkeeping than recomputing a
/// handful of matrix multiplies costs at indie scale.
///
public Matrix4x4 WorldMatrix =>
Parent is null ? Transform.LocalMatrix : Transform.LocalMatrix * Parent.WorldMatrix;
///
/// Reparents this GameObject. Throws if that would create a cycle —
/// checked by walking 's own ancestors for
/// this object, which is cheap next to the cost of silently corrupting
/// the hierarchy.
///
public void SetParent(GameObject? parent)
{
if (ReferenceEquals(parent, this))
throw new InvalidOperationException($"GameObject '{Name}' cannot be its own parent.");
for (var ancestor = parent?.Parent; ancestor is not null; ancestor = ancestor.Parent)
{
if (ReferenceEquals(ancestor, this))
throw new InvalidOperationException(
$"Setting '{parent!.Name}' as the parent of '{Name}' would create a cycle.");
}
if (ReferenceEquals(Parent, parent))
return;
var oldParent = Parent;
oldParent?._children.Remove(this);
Parent = parent;
parent?._children.Add(this);
Owner?.OnReparented(this, oldParent, parent);
}
public T? GetComponent() where T : Component
{
SystemAccessScope.CheckRead(typeof(T));
foreach (var component in _components)
{
if (component is T match)
return match;
}
return null;
}
///
/// Adding a component requires Writes<T>() — checked here,
/// at the structural change. What isn't and can't be checked: mutating
/// a component's own fields after the fact, e.g.
/// go.GetComponent<T>()!.Value = 5. That's a plain field
/// write on a plain object, with nothing to intercept it — see
/// docs/kernel-contract.md §7's note on why components stay plain
/// classes rather than something that could enforce this fully.
///
public T AddComponent() where T : Component, new()
{
SystemAccessScope.CheckWrite(typeof(T));
var component = new T();
_components.Add(component);
Owner?.IndexComponentAdded(this, component);
return component;
}
///
/// Attaches an already-constructed component rather than building an
/// empty one — for a caller that only has a runtime ,
/// not a compile-time T. Scene loading is the reason this
/// exists: it deserializes a component straight from JSON into a real
/// instance via JsonSerializer.Deserialize(json, componentType),
/// and would otherwise need reflection just to call the generic
/// overload above.
///
public Component AddComponent(Component component)
{
SystemAccessScope.CheckWrite(component.GetType());
_components.Add(component);
Owner?.IndexComponentAdded(this, component);
return component;
}
public void RemoveComponent() where T : Component
{
SystemAccessScope.CheckWrite(typeof(T));
for (var i = 0; i < _components.Count; i++)
{
if (_components[i] is not T match)
continue;
_components.RemoveAt(i);
Owner?.IndexComponentRemoved(this, match);
return;
}
}
/// Used only by GameWorld.Destroy, which handles index and
/// roots bookkeeping itself — see the note there on why this bypasses
/// SetParent's cycle check and reparent notification.
internal void DetachFromParent()
{
Parent?._children.Remove(this);
Parent = null;
}
}