Add MrGameEng.Inspector: in-engine ECS debugger overlay
CI / build-test (push) Successful in 1m14s

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:
Leonid Pershin
2026-06-12 12:45:50 +03:00
co-authored by Claude Opus 4.8
parent f791ee6c95
commit 09dbdfad79
9 changed files with 1285 additions and 6 deletions
@@ -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");
}
}