package coffee.liz.ecs; import coffee.liz.ecs.model.Component; import coffee.liz.ecs.model.Entity; import coffee.liz.ecs.model.Query; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** World that updates in {@link System#getDependencies()} topological order. */ @Log4j2 @RequiredArgsConstructor public class DAGWorld implements World { /** All entities in the world. */ protected final Set entities = Collections.synchronizedSet(new HashSet<>()); /** Cache mapping component types to entities having that component. */ private final Map, Set> componentCache = Collections .synchronizedMap(new HashMap<>()); /** Deterministic ID's for spawned entities. */ private final AtomicInteger nextEntityId = new AtomicInteger(0); /** All registered systems. */ protected final Map>, System> systems; /** Ordered list of systems for execution. */ private final List> systemExecutionOrder; @SafeVarargs public DAGWorld(final System... systems) { this.systems = singletonClazzMap(systems); this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems)); log.debug("Executing in order: {}", systemExecutionOrder); } @Override public Entity createEntity() { final Entity entity = Entity.builder().id(nextEntityId.incrementAndGet()).build(); entities.add(entity); return entity; } @Override public void removeEntity(final Entity entity) { entity.getComponentMap().keySet().forEach(componentType -> { final Set cachedEntities = componentCache.get(componentType); if (cachedEntities != null) { cachedEntities.remove(entity); } }); entities.remove(entity); } @Override public Set resolve(final Query query) { final Set> components = query.queryingComponents(); return switch (query.filter()) { case ALL_OF -> resolveAllOf(components); case ANY_OF -> resolveAnyOf(components); case NONE_OF -> resolveNoneOf(components); }; } @Override public void update(final T state, final Duration duration) { systemExecutionOrder.forEach(system -> { refreshComponentCache(); system.update(this, state, duration); }); refreshComponentCache(); } @SuppressWarnings("unchecked") @Override public > S getSystem(final Class system) { return (S) systems.get(system); } private void refreshComponentCache() { componentCache.clear(); entities.forEach(entity -> entity.getComponentMap().keySet().forEach( componentType -> componentCache.computeIfAbsent(componentType, _ -> new HashSet<>()).add(entity))); } private Set resolveAllOf(final Set> components) { if (components.isEmpty()) { return Set.copyOf(entities); } final Class firstType = components.iterator().next(); final Set candidates = componentCache.get(firstType); if (candidates == null) { return Collections.emptySet(); } return candidates.stream().filter(entity -> components.stream().allMatch(entity::has)) .collect(Collectors.toSet()); } private Set resolveAnyOf(final Set> components) { if (components.isEmpty()) { return Collections.emptySet(); } return entities.stream().filter(entity -> components.stream().anyMatch(entity::has)) .collect(Collectors.toSet()); } private Set resolveNoneOf(final Set> components) { if (components.isEmpty()) { return Set.copyOf(entities); } return entities.stream().filter(entity -> components.stream().noneMatch(entity::has)) .collect(Collectors.toSet()); } private List> buildExecutionOrder(final Collection> systems) { if (systems.isEmpty()) { return Collections.emptyList(); } final Map, System> systemMap = systems.stream() .collect(Collectors.toMap(System::getClass, system -> system, (_, b) -> b, LinkedHashMap::new)); final Map, Integer> inDegree = new LinkedHashMap<>(); final Map, Set>> adjacencyList = new LinkedHashMap<>(); systems.forEach(system -> { final Class systemClass = system.getClass(); inDegree.put(systemClass, 0); adjacencyList.put(systemClass, new HashSet<>()); }); systems.forEach(system -> { system.getDependencies().forEach(dependency -> { if (systemMap.containsKey(dependency)) { adjacencyList.get(dependency).add(system.getClass()); inDegree.merge(system.getClass(), 1, Integer::sum); } }); }); // Kahn's algorithm final List> result = new ArrayList<>(); final Queue> queue = new LinkedList<>( inDegree.entrySet().stream().filter(entry -> entry.getValue() == 0).map(Map.Entry::getKey).toList()); while (!queue.isEmpty()) { final Class currentClass = queue.poll(); result.add(systemMap.get(currentClass)); adjacencyList.getOrDefault(currentClass, Collections.emptySet()).forEach(dependent -> { final int newInDegree = inDegree.get(dependent) - 1; inDegree.put(dependent, newInDegree); if (newInDegree == 0) { queue.add(dependent); } }); } if (result.size() != systems.size()) { throw new IllegalStateException("Circular dependency detected in systems"); } return Collections.unmodifiableList(result); } @Override public void dispose() { for (final System system : systemExecutionOrder) { system.dispose(); } componentCache.clear(); entities.clear(); } @SuppressWarnings("unchecked") private static Map, T> singletonClazzMap(final T... singletons) { final boolean areSingletons = Arrays.stream(singletons).map(t -> (Class>) t.getClass()) .distinct().count() == singletons.length; if (!areSingletons) { throw new IllegalArgumentException("Only one instance may be used per clazz"); } return Arrays.stream(singletons).map(t -> Map.entry((Class) t.getClass(), t)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } }