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 (world, _) = SetupGame();
|
||||||
|
|
||||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
var query = new Query<Cell>().Without<Mark>();
|
||||||
var emptyCount = 0;
|
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);
|
emptyCount.Should().Be(9);
|
||||||
}
|
}
|
||||||
|
|
@ -27,15 +28,15 @@ public class GameFlowTests
|
||||||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var query = world.Query().With<Cell>().With<Mark>().Build();
|
|
||||||
var markedCount = 0;
|
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++;
|
markedCount++;
|
||||||
cell.Row.Should().Be(0);
|
iter.Item1.Row.Should().Be(0);
|
||||||
cell.Col.Should().Be(0);
|
iter.Item1.Col.Should().Be(0);
|
||||||
mark.Player.Should().Be(Player.X);
|
iter.Item2.Player.Should().Be(Player.X);
|
||||||
});
|
}
|
||||||
|
|
||||||
markedCount.Should().Be(1);
|
markedCount.Should().Be(1);
|
||||||
}
|
}
|
||||||
|
|
@ -65,12 +66,12 @@ public class GameFlowTests
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
// Only one mark should exist at (0,0), and it should still be X.
|
// 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)>();
|
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()
|
marks.Should().ContainSingle()
|
||||||
.Which.Should().Be((0, 0, Player.X));
|
.Which.Should().Be((0, 0, Player.X));
|
||||||
|
|
@ -168,12 +169,12 @@ public class GameFlowTests
|
||||||
state.CurrentPlayer.Should().Be(Player.X);
|
state.CurrentPlayer.Should().Be(Player.X);
|
||||||
state.MoveCount.Should().Be(2);
|
state.MoveCount.Should().Be(2);
|
||||||
|
|
||||||
var query = world2.Query().With<Cell>().With<Mark>().Build();
|
|
||||||
var marks = new List<(int Row, int Col, Player Player)>();
|
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)]);
|
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>())
|
using (var iter = world.Select<Cell, Mark>())
|
||||||
{
|
{
|
||||||
while (iter.MoveNext())
|
while (iter.MoveNext())
|
||||||
grid[iter.Current1.Row, iter.Current1.Col] =
|
grid[iter.Item1.Row, iter.Item1.Col] =
|
||||||
iter.Current2.Player == Player.X ? 'X' : 'O';
|
iter.Item2.Player == Player.X ? 'X' : 'O';
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int r = 0; r < 3; r++)
|
for (int r = 0; r < 3; r++)
|
||||||
|
|
@ -253,10 +253,10 @@ public class PlayTests
|
||||||
private static List<(int Row, int Col)> GetEmptyCells(World world)
|
private static List<(int Row, int Col)> GetEmptyCells(World world)
|
||||||
{
|
{
|
||||||
var empty = new List<(int, int)>();
|
var empty = new List<(int, int)>();
|
||||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
var query = new Query<Cell>().Without<Mark>();
|
||||||
using var iter = world.Select<Cell>(query);
|
using var iter = world.Select(query);
|
||||||
while (iter.MoveNext())
|
while (iter.MoveNext())
|
||||||
empty.Add((iter.Current1.Row, iter.Current1.Col));
|
empty.Add((iter.Item1.Row, iter.Item1.Col));
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +265,7 @@ public class PlayTests
|
||||||
var grid = new Player[3, 3];
|
var grid = new Player[3, 3];
|
||||||
using (var iter = world.Select<Cell, Mark>())
|
using (var iter = world.Select<Cell, Mark>())
|
||||||
while (iter.MoveNext())
|
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;
|
grid[row, col] = player;
|
||||||
|
|
||||||
for (int r = 0; r < 3; r++)
|
for (int r = 0; r < 3; r++)
|
||||||
|
|
|
||||||
|
|
@ -212,8 +212,8 @@ public class SnapshotTests
|
||||||
{
|
{
|
||||||
while (iter.MoveNext())
|
while (iter.MoveNext())
|
||||||
{
|
{
|
||||||
grid[iter.Current1.Row, iter.Current1.Col] =
|
grid[iter.Item1.Row, iter.Item1.Col] =
|
||||||
iter.Current2.Player == Player.X ? 'X' : 'O';
|
iter.Item2.Player == Player.X ? 'X' : 'O';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,23 @@ public struct PlaceMarkCommand : ICommand
|
||||||
|
|
||||||
public void Execute(World world)
|
public void Execute(World world)
|
||||||
{
|
{
|
||||||
ref var state = ref world.GetSingleton<GameState>();
|
// Read-only check before iteration — never auto-marks.
|
||||||
|
if (world.ReadSingleton<GameState>().Status != GameStatus.Playing)
|
||||||
if (state.Status != GameStatus.Playing)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Find the cell entity at (Row, Col) that has no Mark.
|
// Find the cell entity at (Row, Col) that has no Mark.
|
||||||
// Use the iterator API with early-exit via break.
|
var query = new Query<Cell>().Without<Mark>();
|
||||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
|
||||||
Entity? target = null;
|
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())
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,13 +39,11 @@ public struct PlaceMarkCommand : ICommand
|
||||||
if (target == null)
|
if (target == null)
|
||||||
return; // Cell already occupied or invalid position.
|
return; // Cell already occupied or invalid position.
|
||||||
|
|
||||||
// Place the mark. ComponentAdded is sufficient — MarkModified
|
// Place the mark.
|
||||||
// is only needed when mutating an existing component via GetComponent<T>.
|
|
||||||
world.AddComponent(target.Value, new Mark { Player = state.CurrentPlayer });
|
world.AddComponent(target.Value, new Mark { Player = state.CurrentPlayer });
|
||||||
|
|
||||||
// Advance turn.
|
// Advance turn. Mutations auto-marked via EndIteration — no MarkModified needed.
|
||||||
state.MoveCount++;
|
state.MoveCount++;
|
||||||
state.CurrentPlayer = state.CurrentPlayer == Player.X ? Player.O : Player.X;
|
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)
|
if (world.ReadSingleton<GameState>().Status != GameStatus.Playing)
|
||||||
return;
|
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>();
|
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];
|
var grid = new Player[3, 3];
|
||||||
|
|
||||||
using var iter = world.Select<Cell, Mark>();
|
|
||||||
while (iter.MoveNext())
|
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.
|
// Check rows.
|
||||||
|
|
@ -31,7 +33,7 @@ public class WinCheckSystem : ISystem
|
||||||
{
|
{
|
||||||
if (TryGetWinner(grid[r, 0], grid[r, 1], grid[r, 2], out var winner))
|
if (TryGetWinner(grid[r, 0], grid[r, 1], grid[r, 2], out var winner))
|
||||||
{
|
{
|
||||||
SetWinner(world, ref state, winner);
|
SetWinner(ref state, winner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +43,7 @@ public class WinCheckSystem : ISystem
|
||||||
{
|
{
|
||||||
if (TryGetWinner(grid[0, c], grid[1, c], grid[2, c], out var winner))
|
if (TryGetWinner(grid[0, c], grid[1, c], grid[2, c], out var winner))
|
||||||
{
|
{
|
||||||
SetWinner(world, ref state, winner);
|
SetWinner(ref state, winner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,12 +51,12 @@ public class WinCheckSystem : ISystem
|
||||||
// Check diagonals.
|
// Check diagonals.
|
||||||
if (TryGetWinner(grid[0, 0], grid[1, 1], grid[2, 2], out var diag1))
|
if (TryGetWinner(grid[0, 0], grid[1, 1], grid[2, 2], out var diag1))
|
||||||
{
|
{
|
||||||
SetWinner(world, ref state, diag1);
|
SetWinner(ref state, diag1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (TryGetWinner(grid[0, 2], grid[1, 1], grid[2, 0], out var diag2))
|
if (TryGetWinner(grid[0, 2], grid[1, 1], grid[2, 0], out var diag2))
|
||||||
{
|
{
|
||||||
SetWinner(world, ref state, diag2);
|
SetWinner(ref state, diag2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +64,6 @@ public class WinCheckSystem : ISystem
|
||||||
if (state.MoveCount >= 9)
|
if (state.MoveCount >= 9)
|
||||||
{
|
{
|
||||||
state.Status = GameStatus.Draw;
|
state.Status = GameStatus.Draw;
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,9 +78,8 @@ public class WinCheckSystem : ISystem
|
||||||
return false;
|
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;
|
state.Status = winner == Player.X ? GameStatus.XWon : GameStatus.OWon;
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +44,15 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
"GetSources",
|
"GetSources",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> _singletonMethods = new()
|
||||||
|
{
|
||||||
|
"SetSingleton",
|
||||||
|
"GetSingleton",
|
||||||
|
"ReadSingleton",
|
||||||
|
"HasSingleton",
|
||||||
|
"RemoveSingleton",
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> _forEachNames = new()
|
private static readonly HashSet<string> _forEachNames = new()
|
||||||
{
|
{
|
||||||
"ForEach",
|
"ForEach",
|
||||||
|
|
@ -57,7 +66,7 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
predicate: IsCandidateInvocation,
|
predicate: IsCandidateInvocation,
|
||||||
transform: ExtractComponentType)
|
transform: ExtractComponentType)
|
||||||
.Where(t => t.FullyQualifiedName != null)
|
.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.
|
// Also collect ForEach type arguments from the World class.
|
||||||
var forEachCalls = context.SyntaxProvider
|
var forEachCalls = context.SyntaxProvider
|
||||||
|
|
@ -104,7 +113,7 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
return false;
|
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 _)
|
GeneratorSyntaxContext ctx, CancellationToken _)
|
||||||
{
|
{
|
||||||
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
||||||
|
|
@ -140,7 +149,9 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
bool isRelationship = typeSymbol.AllInterfaces.Any(i =>
|
bool isRelationship = typeSymbol.AllInterfaces.Any(i =>
|
||||||
i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::OECS.IRelationship");
|
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(
|
private static ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship)> ExtractForEachTypes(
|
||||||
|
|
@ -177,21 +188,29 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void GenerateRegistry(SourceProductionContext context,
|
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)
|
ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship)> Right) data)
|
||||||
{
|
{
|
||||||
var (invocations, forEachTypes) = data;
|
var (invocations, forEachTypes) = data;
|
||||||
|
|
||||||
// Collect unique types by fully-qualified name, deduplicating.
|
// 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)
|
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)
|
foreach (var t in forEachTypes)
|
||||||
{
|
{
|
||||||
if (!typeMap.ContainsKey(t.FullyQualifiedName))
|
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)
|
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)
|
if (typeMap.Count == 0)
|
||||||
|
|
@ -213,7 +232,7 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
|
|
||||||
bool first = true;
|
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)
|
if (!first)
|
||||||
sb.AppendLine(",");
|
sb.AppendLine(",");
|
||||||
|
|
@ -225,7 +244,8 @@ public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||||
sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),");
|
sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),");
|
||||||
sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>");
|
sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>");
|
||||||
sb.AppendLine($" world.AddComponent(entity, MessagePackSerializer.Deserialize<{fqn}>(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(" )");
|
sb.Append(" )");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue