Introduce a Chrome-DevTools-style ECS inspector in the UI library. The headless core (ComponentReflector + EcsInspector) enumerates the store's entities grouped by archetype, reflects a selected entity's components and fields, and writes simple scalar edits (number/bool/enum) back through a generic AddComponent — all unit-tested without a GPU. The Myra overlay (EcsInspectorUi + InspectorSystems) adds a side panel with an archetypes -> entities -> fields tree, an editable field view, a world-pick mode with a selection highlight, and a renderer performance tab; wired via scene.UseInspector(renderer), toggled with F1. The UI library now references Graphics (for picking, renderer timings and Transform2D/Sprite). Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f791ee6c95
commit
09dbdfad79
@@ -0,0 +1,89 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Inspector;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Inspector.Tests;
|
||||
|
||||
public class ComponentReflectorTests
|
||||
{
|
||||
private enum Mode
|
||||
{
|
||||
Idle,
|
||||
Run,
|
||||
}
|
||||
|
||||
private struct Probe
|
||||
{
|
||||
public int Count;
|
||||
public float Speed;
|
||||
public bool Active;
|
||||
public Mode State;
|
||||
public Vector2 Offset;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_ClassifiesFieldKinds()
|
||||
{
|
||||
var rows = ComponentReflector.Read(
|
||||
new Probe
|
||||
{
|
||||
Count = 3,
|
||||
Speed = 1.5f,
|
||||
Active = true,
|
||||
State = Mode.Run,
|
||||
Offset = new Vector2(2f, 4f),
|
||||
}
|
||||
);
|
||||
|
||||
Assert.Equal(FieldKind.Number, Find(rows, "Count").Kind);
|
||||
Assert.Equal(FieldKind.Bool, Find(rows, "Active").Kind);
|
||||
Assert.Equal(FieldKind.Enum, Find(rows, "State").Kind);
|
||||
Assert.Equal(FieldKind.Text, Find(rows, "Offset").Kind);
|
||||
Assert.False(Find(rows, "Offset").Editable);
|
||||
Assert.Equal("Run", Find(rows, "State").Value);
|
||||
Assert.Equal("(2, 4)", Find(rows, "Offset").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySet_UpdatesScalarFields()
|
||||
{
|
||||
object boxed = new Probe();
|
||||
|
||||
Assert.True(ComponentReflector.TrySet(boxed, "Count", "42"));
|
||||
Assert.True(ComponentReflector.TrySet(boxed, "Active", "true"));
|
||||
Assert.True(ComponentReflector.TrySet(boxed, "State", "Run"));
|
||||
|
||||
var probe = (Probe)boxed;
|
||||
Assert.Equal(42, probe.Count);
|
||||
Assert.True(probe.Active);
|
||||
Assert.Equal(Mode.Run, probe.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySet_RejectsUnparseableValue()
|
||||
{
|
||||
object boxed = new Probe();
|
||||
Assert.False(ComponentReflector.TrySet(boxed, "Count", "not-a-number"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySet_RejectsNonScalarOrMissingField()
|
||||
{
|
||||
object boxed = new Probe();
|
||||
Assert.False(ComponentReflector.TrySet(boxed, "Offset", "1,2"));
|
||||
Assert.False(ComponentReflector.TrySet(boxed, "Missing", "1"));
|
||||
}
|
||||
|
||||
private static FieldRow Find(IReadOnlyList<FieldRow> rows, string name)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row.Name == name)
|
||||
{
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Xunit.Sdk.XunitException($"field {name} not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using MrGameEng.Inspector;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Inspector.Tests;
|
||||
|
||||
public class EcsInspectorTests
|
||||
{
|
||||
private enum Mode
|
||||
{
|
||||
Idle,
|
||||
Run,
|
||||
}
|
||||
|
||||
private struct Probe : IComponent
|
||||
{
|
||||
public int Count;
|
||||
public bool Active;
|
||||
public Mode State;
|
||||
}
|
||||
|
||||
private struct Marker : IComponent
|
||||
{
|
||||
public int Value;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archetypes_GroupsEntitiesByComponentSet()
|
||||
{
|
||||
var store = new EntityStore();
|
||||
store.CreateEntity(new Probe { Count = 1 });
|
||||
store.CreateEntity(new Probe { Count = 2 });
|
||||
store.CreateEntity(new Marker { Value = 9 });
|
||||
var inspector = new EcsInspector(store);
|
||||
|
||||
var rows = inspector.Archetypes();
|
||||
|
||||
var probeRow = Single(rows, r => r.Signature.Contains("Probe"));
|
||||
Assert.Equal(2, probeRow.Count);
|
||||
Assert.Contains(rows, r => r.Signature.Contains("Marker") && r.Count == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntitiesOf_ReturnsIdsOfArchetype()
|
||||
{
|
||||
var store = new EntityStore();
|
||||
var a = store.CreateEntity(new Probe { Count = 1 });
|
||||
var b = store.CreateEntity(new Probe { Count = 2 });
|
||||
var inspector = new EcsInspector(store);
|
||||
|
||||
var probeRow = Single(inspector.Archetypes(), r => r.Signature.Contains("Probe"));
|
||||
var ids = inspector.EntitiesOf(probeRow.Index);
|
||||
|
||||
Assert.Contains(a.Id, ids);
|
||||
Assert.Contains(b.Id, ids);
|
||||
Assert.Equal(2, ids.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_ReturnsComponentFields()
|
||||
{
|
||||
var store = new EntityStore();
|
||||
var entity = store.CreateEntity(
|
||||
new Probe
|
||||
{
|
||||
Count = 5,
|
||||
Active = true,
|
||||
State = Mode.Run,
|
||||
}
|
||||
);
|
||||
var inspector = new EcsInspector(store);
|
||||
|
||||
var view = Single(inspector.Inspect(entity.Id), c => c.TypeName == "Probe");
|
||||
|
||||
Assert.Equal("5", Field(view, "Count").Value);
|
||||
Assert.Equal("true", Field(view, "Active").Value);
|
||||
Assert.Equal("Run", Field(view, "State").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetField_WritesValueBackToEntity()
|
||||
{
|
||||
var store = new EntityStore();
|
||||
var entity = store.CreateEntity(new Probe { Count = 5 });
|
||||
var inspector = new EcsInspector(store);
|
||||
|
||||
var ok = inspector.SetField(entity.Id, typeof(Probe), "Count", "42");
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(42, entity.GetComponent<Probe>().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetField_RejectsInvalidValue()
|
||||
{
|
||||
var store = new EntityStore();
|
||||
var entity = store.CreateEntity(new Probe { Count = 5 });
|
||||
var inspector = new EcsInspector(store);
|
||||
|
||||
Assert.False(inspector.SetField(entity.Id, typeof(Probe), "Count", "bad"));
|
||||
Assert.Equal(5, entity.GetComponent<Probe>().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_FiltersArchetypesBySignature()
|
||||
{
|
||||
var store = new EntityStore();
|
||||
store.CreateEntity(new Probe());
|
||||
store.CreateEntity(new Marker());
|
||||
var inspector = new EcsInspector(store);
|
||||
|
||||
inspector.SetSearch("Marker");
|
||||
|
||||
var rows = inspector.Archetypes();
|
||||
Assert.Single(rows);
|
||||
Assert.Contains("Marker", rows[0].Signature);
|
||||
}
|
||||
|
||||
private static T Single<T>(IReadOnlyList<T> items, Func<T, bool> predicate)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (predicate(item))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Xunit.Sdk.XunitException("no matching item");
|
||||
}
|
||||
|
||||
private static FieldRow Field(ComponentView view, string name)
|
||||
{
|
||||
foreach (var row in view.Fields)
|
||||
{
|
||||
if (row.Name == name)
|
||||
{
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Xunit.Sdk.XunitException($"field {name} not found");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user