using MoonTools.ECS; namespace Nerfed.Runtime.Scheduling; public sealed class SystemScheduler { private readonly List entries = new List(); private readonly SystemSchedulerOptions options; public SystemScheduler(SystemSchedulerOptions options) { this.options = options ?? new SystemSchedulerOptions(); } public IReadOnlyList Entries => entries; public void Register(MoonTools.ECS.System system, SystemSchedulePhase phase, params string[] dependsOn) { ArgumentNullException.ThrowIfNull(system); string[] dependencies = dependsOn?.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty(); entries.Add(new SystemScheduleEntry(system, phase, dependencies, entries.Count)); } public void Execute(TimeSpan delta) { if (entries.Count == 0) { return; } Dictionary byName = BuildNameIndex(entries); ValidateDependencies(byName, entries); foreach (SystemSchedulePhase phase in Enum.GetValues()) { ExecutePhase(phase, delta, byName); } } private static Dictionary BuildNameIndex(List allEntries) { var byName = new Dictionary(StringComparer.Ordinal); foreach (SystemScheduleEntry entry in allEntries) { if (byName.ContainsKey(entry.Name)) { throw new InvalidOperationException($"Duplicate scheduled system name '{entry.Name}'."); } byName.Add(entry.Name, entry); } return byName; } private void ValidateDependencies(Dictionary byName, List allEntries) { foreach (SystemScheduleEntry entry in allEntries) { foreach (string dependency in entry.DependsOn) { if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry)) { if (options.StrictDependencyValidation) { throw new InvalidOperationException($"Scheduled system '{entry.Name}' depends on missing system '{dependency}'."); } continue; } if (dependencyEntry.Phase > entry.Phase) { throw new InvalidOperationException( $"Scheduled system '{entry.Name}' in phase {entry.Phase} cannot depend on later phase system '{dependency}' in phase {dependencyEntry.Phase}."); } } } } private void ExecutePhase(SystemSchedulePhase phase, TimeSpan delta, Dictionary byName) { List phaseEntries = entries .Where(entry => entry.Phase == phase) .OrderBy(entry => entry.RegistrationIndex) .ToList(); if (phaseEntries.Count == 0) { return; } var completed = new HashSet(StringComparer.Ordinal); while (completed.Count < phaseEntries.Count) { List ready = phaseEntries .Where(entry => !completed.Contains(entry.Name) && DependenciesSatisfied(entry, completed, byName)) .OrderBy(entry => entry.RegistrationIndex) .ToList(); if (ready.Count == 0) { throw new InvalidOperationException($"Cyclic or unsatisfied dependencies detected in phase {phase}."); } List batch = BuildNonConflictingBatch(ready); ExecuteBatch(batch, delta, phase); foreach (SystemScheduleEntry entry in batch) { completed.Add(entry.Name); } } } private static bool DependenciesSatisfied(SystemScheduleEntry entry, HashSet completed, Dictionary byName) { foreach (string dependency in entry.DependsOn) { if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry)) { continue; } if (dependencyEntry.Phase == entry.Phase && !completed.Contains(dependency)) { return false; } } return true; } private static List BuildNonConflictingBatch(List ready) { var batch = new List(ready.Count); for (int i = 0; i < ready.Count; i++) { SystemScheduleEntry candidate = ready[i]; bool conflicts = false; for (int j = 0; j < batch.Count; j++) { if (candidate.Access.ConflictsWith(batch[j].Access)) { conflicts = true; break; } } if (!conflicts) { batch.Add(candidate); } } if (batch.Count == 0) { batch.Add(ready[0]); } return batch; } private void ExecuteBatch(List batch, TimeSpan delta, SystemSchedulePhase phase) { using ProfilerScope phaseScope = new($"Schedule.{phase}.Batch[{batch.Count}]"); if (options.EnableParallelSystemExecution && batch.Count > 1) { Parallel.ForEach(batch, entry => ExecuteSystem(entry, delta)); return; } for (int i = 0; i < batch.Count; i++) { ExecuteSystem(batch[i], delta); } } private static void ExecuteSystem(SystemScheduleEntry entry, TimeSpan delta) { using ProfilerScope scope = new(entry.Name); entry.System.Update(delta); } }