package coffee.liz.ecs; import coffee.liz.ecs.events.EntityEvent; import coffee.liz.ecs.model.Component; import coffee.liz.ecs.model.Entity; import coffee.liz.ecs.model.Query; import coffee.liz.ecs.model.QueryBuilder; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; import lombok.extern.log4j.Log4j2; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; 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.function.Consumer; import java.util.stream.Collectors; @Log4j2 public class DAGWorld implements World { private final Set entities = Collections.synchronizedSet(new HashSet<>()); private final ComponentCache componentCache = new ComponentCache(); private final Consumer entityEventConsumer = componentCache::onEntityEvent; private final AtomicInteger nextEntityId = new AtomicInteger(0); protected final Map, System> systems; private final List systemExecutionOrder; private final QueryBuilder queryBuilder = new QueryBuilder(this); private float elapsedTime = 0f; public DAGWorld(final System... systems) { this.systems = singletonClazzMap(systems); this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems)); log.info("Executing in order: {}", systemExecutionOrder); } @Override public Entity createEntity() { final Entity entity = Entity.builder().id(nextEntityId.incrementAndGet()).build(); entity.subscribe(entityEventConsumer); entities.add(entity); componentCache.addEntity(entity); return entity; } @Override public void removeEntity(final Entity entity) { entity.unsubscribe(entityEventConsumer); componentCache.removeEntity(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 QueryBuilder queryable() { return queryBuilder; } @Override public void update(final float deltaSeconds) { elapsedTime += deltaSeconds; systemExecutionOrder.forEach(system -> system.update(this, deltaSeconds)); } @Override public float getTime() { return elapsedTime; } @SuppressWarnings("unchecked") @Override public S getSystem(final Class system) { return (S) systems.get(system); } private Set resolveAllOf(final Set> components) { if (components.isEmpty()) { return entities; } return componentCache.entitiesMatchingAllOf(components); } private Set resolveAnyOf(final Set> components) { if (components.isEmpty()) { return Collections.emptySet(); } final Set matches = new HashSet<>(); components.forEach(componentType -> matches.addAll(componentCache.entitiesWith(componentType))); return matches; } private Set resolveNoneOf(final Set> components) { if (components.isEmpty()) { return entities; } final Set excluded = new HashSet<>(); components.forEach(componentType -> excluded.addAll(componentCache.entitiesWith(componentType))); final Set result = new HashSet<>(); entities.forEach(entity -> { if (!excluded.contains(entity)) { result.add(entity); } }); return result; } 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, (_sys, b) -> b, LinkedHashMap::new)); final Map, Integer> inDegree = new LinkedHashMap<>(); final Map, Set>> adjacencyList = new LinkedHashMap<>(); systems.forEach(system -> { inDegree.put(system.getClass(), 0); adjacencyList.put(system.getClass(), 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); } })); final List result = new ArrayList<>(); final Queue> queue = new LinkedList<>( inDegree.entrySet().stream().filter(e -> e.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() { systemExecutionOrder.forEach(System::dispose); entities.forEach(entity -> entity.unsubscribe(entityEventConsumer)); componentCache.clear(); entities.clear(); } @SuppressWarnings("unchecked") private static Map, System> singletonClazzMap(final System... singletons) { final boolean areSingletons = Arrays.stream(singletons) .map(Object::getClass) .distinct() .count() == singletons.length; if (!areSingletons) { throw new IllegalArgumentException("Only one instance may be used per clazz"); } return Arrays.stream(singletons) .collect(Collectors.toMap( s -> (Class) s.getClass(), s -> s)); } }