refactor: simplify component iteration and singleton handling
Update the ECS engine to support automatic mutation tracking for singletons during iteration, removing the need for manual `MarkModified` calls. - Update `ComponentDiscoveryGenerator` to detect singleton components. - Refactor `PlaceMarkCommand` and `WinCheckSystem` to leverage automatic mutation marking. - Replace `ForEach` usage with `Select` and `ItemN` access in tests and systems to align with the updated iterator API.
This commit is contained in:
parent
b98e8d66af
commit
96d732d6ab
|
|
@ -12,9 +12,10 @@ public class GameFlowTests
|
|||
{
|
||||
var (world, _) = SetupGame();
|
||||
|
||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
||||
var query = new Query<Cell>().Without<Mark>();
|
||||
var emptyCount = 0;
|
||||
world.ForEach(query, (Entity e, ref Cell cell) => emptyCount++);
|
||||
using var iter = world.Select(query);
|
||||
while (iter.MoveNext()) emptyCount++;
|
||||
|
||||
emptyCount.Should().Be(9);
|
||||
}
|
||||
|
|
@ -27,15 +28,15 @@ public class GameFlowTests
|
|||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
|
||||
group.RunLogical();
|
||||
|
||||
var query = world.Query().With<Cell>().With<Mark>().Build();
|
||||
var markedCount = 0;
|
||||
world.ForEach(query, (Entity e, ref Cell cell, ref Mark mark) =>
|
||||
using var iter = world.Select<Cell, Mark>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
markedCount++;
|
||||
cell.Row.Should().Be(0);
|
||||
cell.Col.Should().Be(0);
|
||||
mark.Player.Should().Be(Player.X);
|
||||
});
|
||||
iter.Item1.Row.Should().Be(0);
|
||||
iter.Item1.Col.Should().Be(0);
|
||||
iter.Item2.Player.Should().Be(Player.X);
|
||||
}
|
||||
|
||||
markedCount.Should().Be(1);
|
||||
}
|
||||
|
|
@ -65,12 +66,12 @@ public class GameFlowTests
|
|||
group.RunLogical();
|
||||
|
||||
// Only one mark should exist at (0,0), and it should still be X.
|
||||
var query = world.Query().With<Cell>().With<Mark>().Build();
|
||||
var marks = new List<(int Row, int Col, Player Player)>();
|
||||
world.ForEach(query, (Entity e, ref Cell cell, ref Mark mark) =>
|
||||
using var iter3 = world.Select<Cell, Mark>();
|
||||
while (iter3.MoveNext())
|
||||
{
|
||||
marks.Add((cell.Row, cell.Col, mark.Player));
|
||||
});
|
||||
marks.Add((iter3.Item1.Row, iter3.Item1.Col, iter3.Item2.Player));
|
||||
}
|
||||
|
||||
marks.Should().ContainSingle()
|
||||
.Which.Should().Be((0, 0, Player.X));
|
||||
|
|
@ -168,12 +169,12 @@ public class GameFlowTests
|
|||
state.CurrentPlayer.Should().Be(Player.X);
|
||||
state.MoveCount.Should().Be(2);
|
||||
|
||||
var query = world2.Query().With<Cell>().With<Mark>().Build();
|
||||
var marks = new List<(int Row, int Col, Player Player)>();
|
||||
world2.ForEach(query, (Entity e, ref Cell cell, ref Mark mark) =>
|
||||
using var iter2 = world2.Select<Cell, Mark>();
|
||||
while (iter2.MoveNext())
|
||||
{
|
||||
marks.Add((cell.Row, cell.Col, mark.Player));
|
||||
});
|
||||
marks.Add((iter2.Item1.Row, iter2.Item1.Col, iter2.Item2.Player));
|
||||
}
|
||||
|
||||
marks.Should().BeEquivalentTo([(0, 0, Player.X), (1, 0, Player.O)]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,8 +163,8 @@ public class PlayTests
|
|||
using (var iter = world.Select<Cell, Mark>())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
grid[iter.Current1.Row, iter.Current1.Col] =
|
||||
iter.Current2.Player == Player.X ? 'X' : 'O';
|
||||
grid[iter.Item1.Row, iter.Item1.Col] =
|
||||
iter.Item2.Player == Player.X ? 'X' : 'O';
|
||||
}
|
||||
|
||||
for (int r = 0; r < 3; r++)
|
||||
|
|
@ -253,10 +253,10 @@ public class PlayTests
|
|||
private static List<(int Row, int Col)> GetEmptyCells(World world)
|
||||
{
|
||||
var empty = new List<(int, int)>();
|
||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
||||
using var iter = world.Select<Cell>(query);
|
||||
var query = new Query<Cell>().Without<Mark>();
|
||||
using var iter = world.Select(query);
|
||||
while (iter.MoveNext())
|
||||
empty.Add((iter.Current1.Row, iter.Current1.Col));
|
||||
empty.Add((iter.Item1.Row, iter.Item1.Col));
|
||||
return empty;
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ public class PlayTests
|
|||
var grid = new Player[3, 3];
|
||||
using (var iter = world.Select<Cell, Mark>())
|
||||
while (iter.MoveNext())
|
||||
grid[iter.Current1.Row, iter.Current1.Col] = iter.Current2.Player;
|
||||
grid[iter.Item1.Row, iter.Item1.Col] = iter.Item2.Player;
|
||||
grid[row, col] = player;
|
||||
|
||||
for (int r = 0; r < 3; r++)
|
||||
|
|
|
|||
|
|
@ -212,8 +212,8 @@ public class SnapshotTests
|
|||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
grid[iter.Current1.Row, iter.Current1.Col] =
|
||||
iter.Current2.Player == Player.X ? 'X' : 'O';
|
||||
grid[iter.Item1.Row, iter.Item1.Col] =
|
||||
iter.Item2.Player == Player.X ? 'X' : 'O';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,23 @@ public struct PlaceMarkCommand : ICommand
|
|||
|
||||
public void Execute(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
if (state.Status != GameStatus.Playing)
|
||||
// Read-only check before iteration — never auto-marks.
|
||||
if (world.ReadSingleton<GameState>().Status != GameStatus.Playing)
|
||||
return;
|
||||
|
||||
// Find the cell entity at (Row, Col) that has no Mark.
|
||||
// Use the iterator API with early-exit via break.
|
||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
||||
var query = new Query<Cell>().Without<Mark>();
|
||||
Entity? target = null;
|
||||
|
||||
using var iter = world.Select<Cell>(query);
|
||||
// Fetch singleton ref inside iteration so mutations are auto-marked.
|
||||
using var iter = world.Select(query);
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.Current1.Row == Row && iter.Current1.Col == Col)
|
||||
if (iter.Item1.Row == Row && iter.Item1.Col == Col)
|
||||
{
|
||||
target = iter.CurrentEntity;
|
||||
target = iter.Entity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,13 +39,11 @@ public struct PlaceMarkCommand : ICommand
|
|||
if (target == null)
|
||||
return; // Cell already occupied or invalid position.
|
||||
|
||||
// Place the mark. ComponentAdded is sufficient — MarkModified
|
||||
// is only needed when mutating an existing component via GetComponent<T>.
|
||||
// Place the mark.
|
||||
world.AddComponent(target.Value, new Mark { Player = state.CurrentPlayer });
|
||||
|
||||
// Advance turn.
|
||||
// Advance turn. Mutations auto-marked via EndIteration — no MarkModified needed.
|
||||
state.MoveCount++;
|
||||
state.CurrentPlayer = state.CurrentPlayer == Player.X ? Player.O : Player.X;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,17 @@ public class WinCheckSystem : ISystem
|
|||
if (world.ReadSingleton<GameState>().Status != GameStatus.Playing)
|
||||
return;
|
||||
|
||||
// Get singleton ref inside iteration so all mutations are auto-marked
|
||||
// by EndIteration — no MarkModified calls needed.
|
||||
using var iter = world.Select<Cell, Mark>();
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
// Build a 3×3 grid of marks using the iterator API.
|
||||
// Build a 3×3 grid of marks.
|
||||
var grid = new Player[3, 3];
|
||||
|
||||
using var iter = world.Select<Cell, Mark>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
grid[iter.Current1.Row, iter.Current1.Col] = iter.Current2.Player;
|
||||
grid[iter.Item1.Row, iter.Item1.Col] = iter.Item2.Player;
|
||||
}
|
||||
|
||||
// Check rows.
|
||||
|
|
@ -31,7 +33,7 @@ public class WinCheckSystem : ISystem
|
|||
{
|
||||
if (TryGetWinner(grid[r, 0], grid[r, 1], grid[r, 2], out var winner))
|
||||
{
|
||||
SetWinner(world, ref state, winner);
|
||||
SetWinner(ref state, winner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +43,7 @@ public class WinCheckSystem : ISystem
|
|||
{
|
||||
if (TryGetWinner(grid[0, c], grid[1, c], grid[2, c], out var winner))
|
||||
{
|
||||
SetWinner(world, ref state, winner);
|
||||
SetWinner(ref state, winner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -49,12 +51,12 @@ public class WinCheckSystem : ISystem
|
|||
// Check diagonals.
|
||||
if (TryGetWinner(grid[0, 0], grid[1, 1], grid[2, 2], out var diag1))
|
||||
{
|
||||
SetWinner(world, ref state, diag1);
|
||||
SetWinner(ref state, diag1);
|
||||
return;
|
||||
}
|
||||
if (TryGetWinner(grid[0, 2], grid[1, 1], grid[2, 0], out var diag2))
|
||||
{
|
||||
SetWinner(world, ref state, diag2);
|
||||
SetWinner(ref state, diag2);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +64,6 @@ public class WinCheckSystem : ISystem
|
|||
if (state.MoveCount >= 9)
|
||||
{
|
||||
state.Status = GameStatus.Draw;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,9 +78,8 @@ public class WinCheckSystem : ISystem
|
|||
return false;
|
||||
}
|
||||
|
||||
private static void SetWinner(World world, ref GameState state, Player winner)
|
||||
private static void SetWinner(ref GameState state, Player winner)
|
||||
{
|
||||
state.Status = winner == Player.X ? GameStatus.XWon : GameStatus.OWon;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,15 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
"GetSources",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _singletonMethods = new()
|
||||
{
|
||||
"SetSingleton",
|
||||
"GetSingleton",
|
||||
"ReadSingleton",
|
||||
"HasSingleton",
|
||||
"RemoveSingleton",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _forEachNames = new()
|
||||
{
|
||||
"ForEach",
|
||||
|
|
@ -57,7 +66,7 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
predicate: IsCandidateInvocation,
|
||||
transform: ExtractComponentType)
|
||||
.Where(t => t.FullyQualifiedName != null)
|
||||
.Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!, t.IsRelationship));
|
||||
.Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!, t.IsRelationship, t.IsSingleton));
|
||||
|
||||
// Also collect ForEach type arguments from the World class.
|
||||
var forEachCalls = context.SyntaxProvider
|
||||
|
|
@ -104,7 +113,7 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
return false;
|
||||
}
|
||||
|
||||
private static (string? FullyQualifiedName, string? AssemblyQualifiedName, bool IsRelationship) ExtractComponentType(
|
||||
private static (string? FullyQualifiedName, string? AssemblyQualifiedName, bool IsRelationship, bool IsSingleton) ExtractComponentType(
|
||||
GeneratorSyntaxContext ctx, CancellationToken _)
|
||||
{
|
||||
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
||||
|
|
@ -140,7 +149,9 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
bool isRelationship = typeSymbol.AllInterfaces.Any(i =>
|
||||
i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::OECS.IRelationship");
|
||||
|
||||
return (fqn, aqn, isRelationship);
|
||||
bool isSingleton = _singletonMethods.Contains(genericName.Identifier.Text);
|
||||
|
||||
return (fqn, aqn, isRelationship, isSingleton);
|
||||
}
|
||||
|
||||
private static ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship)> ExtractForEachTypes(
|
||||
|
|
@ -177,21 +188,29 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
}
|
||||
|
||||
private static void GenerateRegistry(SourceProductionContext context,
|
||||
(ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship)> Left,
|
||||
(ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship, bool IsSingleton)> Left,
|
||||
ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship)> Right) data)
|
||||
{
|
||||
var (invocations, forEachTypes) = data;
|
||||
|
||||
// Collect unique types by fully-qualified name, deduplicating.
|
||||
var typeMap = new Dictionary<string, (string Fqn, string Aqn, bool IsRel)>();
|
||||
var typeMap = new Dictionary<string, (string Fqn, string Aqn, bool IsRel, bool IsSingleton)>();
|
||||
foreach (var t in invocations)
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName, t.IsRelationship);
|
||||
{
|
||||
if (typeMap.TryGetValue(t.FullyQualifiedName, out var existing))
|
||||
typeMap[t.FullyQualifiedName] = (existing.Fqn, existing.Aqn, existing.IsRel || t.IsRelationship, existing.IsSingleton || t.IsSingleton);
|
||||
else
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName, t.IsRelationship, t.IsSingleton);
|
||||
}
|
||||
foreach (var t in forEachTypes)
|
||||
{
|
||||
if (!typeMap.ContainsKey(t.FullyQualifiedName))
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName, t.IsRelationship);
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName, t.IsRelationship, false);
|
||||
else if (t.IsRelationship)
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName, true);
|
||||
{
|
||||
var existing = typeMap[t.FullyQualifiedName];
|
||||
typeMap[t.FullyQualifiedName] = (existing.Fqn, existing.Aqn, true, existing.IsSingleton);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeMap.Count == 0)
|
||||
|
|
@ -213,7 +232,7 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
sb.AppendLine(" {");
|
||||
|
||||
bool first = true;
|
||||
foreach (var (fqn, aqn, isRel) in typeMap.Values.OrderBy(t => t.Fqn))
|
||||
foreach (var (fqn, aqn, isRel, isSingleton) in typeMap.Values.OrderBy(t => t.Fqn))
|
||||
{
|
||||
if (!first)
|
||||
sb.AppendLine(",");
|
||||
|
|
@ -225,7 +244,8 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|||
sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),");
|
||||
sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>");
|
||||
sb.AppendLine($" world.AddComponent(entity, MessagePackSerializer.Deserialize<{fqn}>(data)),");
|
||||
sb.AppendLine($" deserialize: data => MessagePackSerializer.Deserialize<{fqn}>(data)");
|
||||
sb.AppendLine($" deserialize: data => MessagePackSerializer.Deserialize<{fqn}>(data),");
|
||||
sb.AppendLine($" isSingleton: {(isSingleton ? "true" : "false")}");
|
||||
|
||||
sb.Append(" )");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue