summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2026-02-28 14:08:49 -0800
committerElizabeth Alexander Hunt <me@liz.coffee>2026-02-28 14:08:49 -0800
commit8412efda977c1c76885eae1d0b4a721cf71162f2 (patch)
tree4ff20bc346fd24aeb5881ea06855d7bea5f5d162
parent87c8a1e15e399d29f42b41a4ccb66a84c5f6bb9a (diff)
downloaddyl-8412efda977c1c76885eae1d0b4a721cf71162f2.tar.gz
dyl-8412efda977c1c76885eae1d0b4a721cf71162f2.zip
Upgrading JDK and adding Observable interface
-rw-r--r--build.gradle3
-rw-r--r--core/src/main/java/coffee/liz/dyl/components/StressComponent.java16
-rw-r--r--core/src/main/java/coffee/liz/dyl/systems/StressSystem.java31
-rw-r--r--core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java6
-rw-r--r--core/src/main/java/coffee/liz/ecs/ComponentCache.java124
-rw-r--r--core/src/main/java/coffee/liz/ecs/DAGWorld.java63
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/ComponentAdded.java16
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java21
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/EntityEvent.java13
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/Hook.java13
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/Observable.java34
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Entity.java41
-rw-r--r--core/src/test/java/coffee/liz/ecs/DAGWorldTest.java20
-rw-r--r--core/src/test/java/coffee/liz/ecs/model/EntityTest.java30
-rw-r--r--gradle/gradle-daemon-jvm.properties22
-rw-r--r--lwjgl3/build.gradle14
16 files changed, 404 insertions, 63 deletions
diff --git a/build.gradle b/build.gradle
index 817f7a3..79053e4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,7 +27,8 @@ allprojects {
configure(subprojects) {
apply plugin: 'java-library'
- java.sourceCompatibility = 17
+ java.sourceCompatibility = JavaVersion.VERSION_24
+ java.targetCompatibility = JavaVersion.VERSION_24
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.42'
diff --git a/core/src/main/java/coffee/liz/dyl/components/StressComponent.java b/core/src/main/java/coffee/liz/dyl/components/StressComponent.java
new file mode 100644
index 0000000..f22dfaa
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/components/StressComponent.java
@@ -0,0 +1,16 @@
+package coffee.liz.dyl.components;
+
+import coffee.liz.ecs.model.Component;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@RequiredArgsConstructor
+public class StressComponent implements Component {
+ private final String componentData;
+ public StressComponent() {
+ componentData = IntStream.range(0, 10).mapToObj(Integer::toString).collect(Collectors.joining());
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/systems/StressSystem.java b/core/src/main/java/coffee/liz/dyl/systems/StressSystem.java
new file mode 100644
index 0000000..850a9f3
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/systems/StressSystem.java
@@ -0,0 +1,31 @@
+package coffee.liz.dyl.systems;
+
+import coffee.liz.dyl.FrameState;
+import coffee.liz.dyl.components.StressComponent;
+import coffee.liz.dyl.components.Velocity;
+import coffee.liz.ecs.math.Vec2f;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+
+import java.util.Collection;
+import java.util.List;
+
+public class StressSystem implements System<FrameState> {
+
+ @Override
+ public Collection<Class<? extends System<FrameState>>> getDependencies() {
+ return List.of();
+ }
+
+ @Override
+ public void update(World<FrameState> world, FrameState state, float deltaSeconds) {
+ world.queryable().allOf().forEach(e -> {
+ if (Math.random() > 0.5) {
+ e.add(new StressComponent());
+ e.add(new Velocity(new Vec2f(0.5f - (float) Math.random(), 0.5f - (float) Math.random())));
+ } else if (e.has(StressComponent.class)){
+ e.remove(StressComponent.class);
+ }
+ });
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java
index c3e1c2d..475694e 100644
--- a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java
+++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java
@@ -7,6 +7,7 @@ import coffee.liz.dyl.entities.PlayerFactory;
import coffee.liz.dyl.systems.InputSystem;
import coffee.liz.dyl.systems.IntegrationSystem;
import coffee.liz.dyl.systems.RenderSystem;
+import coffee.liz.dyl.systems.StressSystem;
import coffee.liz.ecs.DAGWorld;
import coffee.liz.ecs.math.Vec2f;
import coffee.liz.ecs.math.Vec2i;
@@ -16,9 +17,10 @@ public class DylGameWorld extends DAGWorld<FrameState> {
super(
new InputSystem(),
new IntegrationSystem(),
- new RenderSystem(game.getBatch(), game.getViewport())
+ new RenderSystem(game.getBatch(), game.getViewport()),
+ new StressSystem()
);
- for (int i = 0; i < 8000; i++) {
+ for (int i = 0; i < 16_000; i++) {
PlayerFactory.addTo(this)
.add(new BoundingBox(new Vec2f(i / 200f, i/ 200f), new Vec2i(1, 1), 1));
}
diff --git a/core/src/main/java/coffee/liz/ecs/ComponentCache.java b/core/src/main/java/coffee/liz/ecs/ComponentCache.java
new file mode 100644
index 0000000..70487d6
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/ComponentCache.java
@@ -0,0 +1,124 @@
+package coffee.liz.ecs;
+
+import coffee.liz.ecs.events.ComponentAdded;
+import coffee.liz.ecs.events.ComponentRemoved;
+import coffee.liz.ecs.events.EntityEvent;
+import coffee.liz.ecs.model.Component;
+import coffee.liz.ecs.model.Entity;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Incremental cache for component-to-entity lookups. */
+public final class ComponentCache {
+ private final Map<Class<? extends Component>, Set<Entity>> entitiesByComponent = new HashMap<>();
+
+ /**
+ * Index all current components on a newly tracked entity.
+ *
+ * @param entity
+ * entity to index
+ */
+ public void addEntity(final Entity entity) {
+ entity.componentTypes().forEach(componentType -> addComponentEntry(componentType, entity));
+ }
+
+ /**
+ * Remove all current components for a no-longer-tracked entity.
+ *
+ * @param entity
+ * entity to remove from the cache
+ */
+ public void removeEntity(final Entity entity) {
+ entity.componentTypes().forEach(componentType -> removeComponentEntry(componentType, entity));
+ }
+
+ /**
+ * Apply a mutation event to the cache.
+ *
+ * @param event
+ * mutation event emitted by an entity
+ */
+ public void onEntityEvent(final EntityEvent event) {
+ switch (event) {
+ case ComponentAdded componentAdded:
+ addComponentEntry(componentAdded.getComponentClazz(), componentAdded.getEntity());
+ break;
+ case ComponentRemoved componentRemoved:
+ removeComponentEntry(componentRemoved.getComponentClazz(), componentRemoved.getEntity());
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Get all entities that contain a component type.
+ *
+ * @param componentType
+ * component class to query
+ * @return entities containing that component
+ */
+ public Set<Entity> entitiesWith(final Class<? extends Component> componentType) {
+ return entitiesByComponent.getOrDefault(componentType, Collections.emptySet());
+ }
+
+ /**
+ * Resolve entities that contain every queried component type.
+ *
+ * @param componentTypes
+ * required component classes
+ * @return entities that have all queried component types
+ */
+ public Set<Entity> entitiesMatchingAllOf(final Collection<Class<? extends Component>> componentTypes) {
+ if (componentTypes.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ Set<Entity> smallestBucket = null;
+ for (final Class<? extends Component> componentType : componentTypes) {
+ final Set<Entity> entities = entitiesWith(componentType);
+ if (entities.isEmpty()) {
+ return Collections.emptySet();
+ }
+ if (smallestBucket == null || entities.size() < smallestBucket.size()) {
+ smallestBucket = entities;
+ }
+ }
+
+ final Set<Entity> matches = new HashSet<>();
+ if (smallestBucket == null) {
+ return matches;
+ }
+
+ for (final Entity entity : smallestBucket) {
+ if (entity.hasAll(componentTypes)) {
+ matches.add(entity);
+ }
+ }
+ return matches;
+ }
+
+ /**
+ * Remove all cached component indexes.
+ */
+ public void clear() {
+ entitiesByComponent.clear();
+ }
+
+ private void addComponentEntry(final Class<? extends Component> componentType, final Entity entity) {
+ entitiesByComponent.computeIfAbsent(componentType, _unused -> new HashSet<>()).add(entity);
+ }
+
+ private void removeComponentEntry(final Class<? extends Component> componentType, final Entity entity) {
+ final Set<Entity> entities = entitiesByComponent.get(componentType);
+ entities.remove(entity);
+ if (entities.isEmpty()) {
+ entitiesByComponent.remove(componentType);
+ }
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
index 1075e5e..a6a11db 100644
--- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java
+++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
@@ -1,5 +1,7 @@
package coffee.liz.ecs;
+import coffee.liz.ecs.events.EntityEvent;
+import coffee.liz.ecs.events.Hook;
import coffee.liz.ecs.model.Component;
import coffee.liz.ecs.model.Entity;
import coffee.liz.ecs.model.Query;
@@ -13,7 +15,6 @@ 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;
@@ -31,9 +32,11 @@ public class DAGWorld<T> implements World<T> {
/** All entities in the world. */
protected final Set<Entity> entities = Collections.synchronizedSet(new HashSet<>());
- /** Cache mapping component types to entities having that component. */
- private final Map<Class<? extends Component>, Set<Entity>> componentCache = Collections
- .synchronizedMap(new HashMap<>());
+ /** Incremental cache mapping component types to entities having that component. */
+ private final ComponentCache componentCache = new ComponentCache();
+
+ /** Shared world listener to keep cache synchronized with entity component changes. */
+ private final Hook<EntityEvent> cacheUpdateHook = componentCache::onEntityEvent;
/** Deterministic ID's for spawned entities. */
private final AtomicInteger nextEntityId = new AtomicInteger(0);
@@ -48,24 +51,22 @@ public class DAGWorld<T> implements World<T> {
public DAGWorld(final System<T>... systems) {
this.systems = singletonClazzMap(systems);
this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems));
- log.debug("Executing in order: {}", systemExecutionOrder);
+ log.info("Executing in order: {}", systemExecutionOrder);
}
@Override
public Entity createEntity() {
final Entity entity = Entity.builder().id(nextEntityId.incrementAndGet()).build();
+ entity.subscribe(cacheUpdateHook);
entities.add(entity);
+ componentCache.addEntity(entity);
return entity;
}
@Override
public void removeEntity(final Entity entity) {
- entity.getComponentMap().keySet().forEach(componentType -> {
- final Set<Entity> cachedEntities = componentCache.get(componentType);
- if (cachedEntities != null) {
- cachedEntities.remove(entity);
- }
- });
+ entity.unsubscribe(cacheUpdateHook);
+ componentCache.removeEntity(entity);
entities.remove(entity);
}
@@ -81,11 +82,7 @@ public class DAGWorld<T> implements World<T> {
@Override
public void update(final T state, final float deltaSeconds) {
- systemExecutionOrder.forEach(system -> {
- refreshComponentCache();
- system.update(this, state, deltaSeconds);
- });
- refreshComponentCache();
+ systemExecutionOrder.forEach(system -> system.update(this, state, deltaSeconds));
}
@SuppressWarnings("unchecked")
@@ -94,25 +91,11 @@ public class DAGWorld<T> implements World<T> {
return (S) systems.get(system);
}
- private void refreshComponentCache() {
- componentCache.clear();
- entities.forEach(entity -> entity.getComponentMap().keySet().forEach(
- componentType -> componentCache.computeIfAbsent(componentType, _comp -> new HashSet<>()).add(entity)));
- }
-
private Set<Entity> resolveAllOf(final Set<Class<? extends Component>> components) {
if (components.isEmpty()) {
return Set.copyOf(entities);
}
-
- final Class<? extends Component> firstType = components.iterator().next();
- final Set<Entity> candidates = componentCache.get(firstType);
- if (candidates == null) {
- return Collections.emptySet();
- }
-
- return candidates.stream().filter(entity -> components.stream().allMatch(entity::has))
- .collect(Collectors.toSet());
+ return componentCache.entitiesMatchingAllOf(components);
}
private Set<Entity> resolveAnyOf(final Set<Class<? extends Component>> components) {
@@ -120,8 +103,9 @@ public class DAGWorld<T> implements World<T> {
return Collections.emptySet();
}
- return entities.stream().filter(entity -> components.stream().anyMatch(entity::has))
- .collect(Collectors.toSet());
+ final Set<Entity> matches = new HashSet<>();
+ components.forEach(componentType -> matches.addAll(componentCache.entitiesWith(componentType)));
+ return matches;
}
private Set<Entity> resolveNoneOf(final Set<Class<? extends Component>> components) {
@@ -129,8 +113,16 @@ public class DAGWorld<T> implements World<T> {
return Set.copyOf(entities);
}
- return entities.stream().filter(entity -> components.stream().noneMatch(entity::has))
- .collect(Collectors.toSet());
+ final Set<Entity> excluded = new HashSet<>();
+ components.forEach(componentType -> excluded.addAll(componentCache.entitiesWith(componentType)));
+
+ final Set<Entity> result = new HashSet<>();
+ entities.forEach(entity -> {
+ if (!excluded.contains(entity)) {
+ result.add(entity);
+ }
+ });
+ return result;
}
private List<System<T>> buildExecutionOrder(final Collection<System<T>> systems) {
@@ -189,6 +181,7 @@ public class DAGWorld<T> implements World<T> {
for (final System<T> system : systemExecutionOrder) {
system.dispose();
}
+ entities.forEach(entity -> entity.unsubscribe(cacheUpdateHook));
componentCache.clear();
entities.clear();
}
diff --git a/core/src/main/java/coffee/liz/ecs/events/ComponentAdded.java b/core/src/main/java/coffee/liz/ecs/events/ComponentAdded.java
new file mode 100644
index 0000000..6837bfc
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/events/ComponentAdded.java
@@ -0,0 +1,16 @@
+package coffee.liz.ecs.events;
+
+import coffee.liz.ecs.model.Component;
+import coffee.liz.ecs.model.Entity;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Emitted when an entity receives a component.
+ */
+@RequiredArgsConstructor
+@Getter
+public class ComponentAdded implements EntityEvent {
+ private final Entity entity;
+ private final Class<? extends Component> componentClazz;
+}
diff --git a/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java b/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java
new file mode 100644
index 0000000..25047b6
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java
@@ -0,0 +1,21 @@
+package coffee.liz.ecs.events;
+
+import coffee.liz.ecs.model.Component;
+import coffee.liz.ecs.model.Entity;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Emitted when an entity loses a component.
+ *
+ * @param entity
+ * entity that lost a component
+ * @param componentClazz
+ * class key of the component that was removed
+ */
+@RequiredArgsConstructor
+@Getter
+public class ComponentRemoved implements EntityEvent {
+ private final Entity entity;
+ private final Class<? extends Component> componentClazz;
+}
diff --git a/core/src/main/java/coffee/liz/ecs/events/EntityEvent.java b/core/src/main/java/coffee/liz/ecs/events/EntityEvent.java
new file mode 100644
index 0000000..cae20e6
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/events/EntityEvent.java
@@ -0,0 +1,13 @@
+package coffee.liz.ecs.events;
+
+import coffee.liz.ecs.model.Entity;
+
+/** Marker interface for events emitted by {@link Entity}. */
+public interface EntityEvent {
+ /**
+ * Get the entity that emitted this event.
+ *
+ * @return source {@link Entity}
+ */
+ Entity getEntity();
+}
diff --git a/core/src/main/java/coffee/liz/ecs/events/Hook.java b/core/src/main/java/coffee/liz/ecs/events/Hook.java
new file mode 100644
index 0000000..8f91aad
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/events/Hook.java
@@ -0,0 +1,13 @@
+package coffee.liz.ecs.events;
+
+import java.util.function.Consumer;
+
+/**
+ * Event callback used by {@link Observable}.
+ *
+ * @param <E>
+ * consumed event type
+ */
+@FunctionalInterface
+public interface Hook<E> extends Consumer<E> {
+}
diff --git a/core/src/main/java/coffee/liz/ecs/events/Observable.java b/core/src/main/java/coffee/liz/ecs/events/Observable.java
new file mode 100644
index 0000000..d849407
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/events/Observable.java
@@ -0,0 +1,34 @@
+package coffee.liz.ecs.events;
+
+/**
+ * Minimal observable contract for event emission.
+ *
+ * @param <E>
+ * emitted event type
+ */
+public interface Observable<E> {
+ /**
+ * Register a hook to receive future events.
+ *
+ * @param hook
+ * callback to invoke for each emitted event
+ * @return the registered hook
+ */
+ Hook<E> subscribe(Hook<E> hook);
+
+ /**
+ * Remove a previously registered hook.
+ *
+ * @param hook
+ * callback to remove
+ */
+ void unsubscribe(Hook<E> hook);
+
+ /**
+ * Publish an event to all currently subscribed hooks.
+ *
+ * @param event
+ * event to publish
+ */
+ void emit(E event);
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Entity.java b/core/src/main/java/coffee/liz/ecs/model/Entity.java
index 7dab667..4984d5d 100644
--- a/core/src/main/java/coffee/liz/ecs/model/Entity.java
+++ b/core/src/main/java/coffee/liz/ecs/model/Entity.java
@@ -1,14 +1,22 @@
package coffee.liz.ecs.model;
+import coffee.liz.ecs.events.ComponentAdded;
+import coffee.liz.ecs.events.ComponentRemoved;
+import coffee.liz.ecs.events.EntityEvent;
+import coffee.liz.ecs.events.Hook;
+import coffee.liz.ecs.events.Observable;
+
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
@@ -17,7 +25,7 @@ import java.util.Set;
@RequiredArgsConstructor
@AllArgsConstructor
@Data
-public class Entity {
+public class Entity implements Observable<EntityEvent> {
/** Unique id. */
private final int id;
@@ -25,6 +33,10 @@ public class Entity {
@Builder.Default
private Map<Class<? extends Component>, Component> componentMap = Collections.synchronizedMap(new HashMap<>());
+ @Builder.Default
+ /** Event subscribers for entity mutation events. */
+ private transient Set<Hook<EntityEvent>> hooks = Collections.newSetFromMap(new IdentityHashMap<>());
+
/**
* Check if entity has component type.
*
@@ -76,7 +88,9 @@ public class Entity {
* @return this {@link Entity} for chaining
*/
public <C extends Component> Entity add(final C component) {
- componentMap.put(component.getKey(), component);
+ final Class<? extends Component> componentType = component.getKey();
+ componentMap.put(componentType, component);
+ emit(new ComponentAdded(this, componentType));
return this;
}
@@ -90,7 +104,9 @@ public class Entity {
* @return this {@link Entity} for chaining
*/
public <C extends Component> Entity remove(final Class<C> componentType) {
- componentMap.remove(componentType);
+ if (componentMap.remove(componentType) != null) {
+ emit(new ComponentRemoved(this, componentType));
+ }
return this;
}
@@ -102,4 +118,23 @@ public class Entity {
public Set<Class<? extends Component>> componentTypes() {
return componentMap.keySet();
}
+
+ /** Subscribe to component mutation events emitted by this entity. */
+ @Override
+ public Hook<EntityEvent> subscribe(final Hook<EntityEvent> hook) {
+ hooks.add(hook);
+ return hook;
+ }
+
+ /** Remove a previously subscribed mutation hook. */
+ @Override
+ public void unsubscribe(final Hook<EntityEvent> hook) {
+ hooks.remove(hook);
+ }
+
+ /** Emit a mutation event to all current subscribers. */
+ @Override
+ public void emit(final EntityEvent event) {
+ new ArrayList<>(hooks).forEach(hook -> hook.accept(event));
+ }
}
diff --git a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
index cf6cdad..4825e36 100644
--- a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
+++ b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
@@ -68,19 +68,31 @@ public class DAGWorldTest {
}
@Test
- public void updateRefreshesComponentCacheAfterEntityMutations() {
+ public void cacheTracksComponentMutationsViaEntityEvents() {
final DAGWorld<String> world = new DAGWorld<>();
final Entity subject = world.createEntity();
- world.update("state", 0);
assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty());
subject.add(new PositionComponent());
- world.update("state", 0);
assertEquals(1, world.resolve(Query.allOf(PositionComponent.class)).size());
subject.remove(PositionComponent.class);
- world.update("state", 0);
+ assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty());
+ }
+
+ @Test
+ public void removedEntityNoLongerMutatesWorldCache() {
+ final DAGWorld<String> world = new DAGWorld<>();
+ final Entity subject = world.createEntity();
+
+ subject.add(new PositionComponent());
+ assertEquals(Set.of(subject), world.resolve(Query.allOf(PositionComponent.class)));
+
+ world.removeEntity(subject);
+ subject.remove(PositionComponent.class);
+ subject.add(new PositionComponent());
+
assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty());
}
diff --git a/core/src/test/java/coffee/liz/ecs/model/EntityTest.java b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java
index a8fd1e3..bb17296 100644
--- a/core/src/test/java/coffee/liz/ecs/model/EntityTest.java
+++ b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java
@@ -6,6 +6,11 @@ import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import coffee.liz.ecs.events.ComponentAdded;
+import coffee.liz.ecs.events.ComponentRemoved;
+import coffee.liz.ecs.events.EntityEvent;
+import coffee.liz.ecs.events.Hook;
+
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
@@ -15,6 +20,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.util.Collection;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
final class EntityTest {
@@ -75,6 +81,30 @@ final class EntityTest {
assertTrue(entity.componentTypes().isEmpty());
}
+ @Test
+ public void subscribeReceivesEmittedEvents() {
+ final Entity entity = Entity.builder().id(51).build();
+ final AtomicInteger addedCount = new AtomicInteger(0);
+ final AtomicInteger removedCount = new AtomicInteger(0);
+
+ final Hook<EntityEvent> hook = entity.subscribe(event -> {
+ if (event instanceof ComponentAdded) {
+ addedCount.incrementAndGet();
+ }
+ if (event instanceof ComponentRemoved) {
+ removedCount.incrementAndGet();
+ }
+ });
+
+ entity.add(new AlphaComponent("a"));
+ entity.remove(AlphaComponent.class);
+
+ assertEquals(1, addedCount.get());
+ assertEquals(1, removedCount.get());
+
+ entity.unsubscribe(hook);
+ }
+
private record AlphaComponent(String name) implements Component {
}
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
index e5d86a4..1ad40d6 100644
--- a/gradle/gradle-daemon-jvm.properties
+++ b/gradle/gradle-daemon-jvm.properties
@@ -1,12 +1,12 @@
#This file is generated by updateDaemonJvm
-toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73c462e34475aeb6509ab7ba3eda218f/redirect
-toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/54001d0b636ad500b432d20ef3d580d0/redirect
-toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73c462e34475aeb6509ab7ba3eda218f/redirect
-toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/54001d0b636ad500b432d20ef3d580d0/redirect
-toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29c55e6bad8a0049163f0184625cecd9/redirect
-toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/3ac7a5361c25c0b23d933f44bdb0abd9/redirect
-toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73c462e34475aeb6509ab7ba3eda218f/redirect
-toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/54001d0b636ad500b432d20ef3d580d0/redirect
-toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/dd5b582862cacd4b8e0d82037f92a53f/redirect
-toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/16008c489780dfb402c44316e612a16c/redirect
-toolchainVersion=17
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/ce3ff383a4a3e769ac7d9ca5903aa698/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/44715d7d372da8362a7c7e78c011e897/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/4f4ebe4f162f6deb29540c4ebe629d79/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/79d5995ef1c3e4df39a3b2f545cada5e/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/826893cbcf0f86d8eb0975e2fb0788f7/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/747369971f89e1f6bc182b737d168306/redirect
+toolchainVersion=24
diff --git a/lwjgl3/build.gradle b/lwjgl3/build.gradle
index 26356c4..76f521b 100644
--- a/lwjgl3/build.gradle
+++ b/lwjgl3/build.gradle
@@ -22,10 +22,10 @@ sourceSets.main.resources.srcDirs += [ rootProject.file('assets').path ]
application.mainClass = 'coffee.liz.dyl.lwjgl3.Lwjgl3Launcher'
application.applicationName = appName
eclipse.project.name = appName + '-lwjgl3'
-java.sourceCompatibility = 17
-java.targetCompatibility = 17
+java.sourceCompatibility = JavaVersion.VERSION_24
+java.targetCompatibility = JavaVersion.VERSION_24
if (JavaVersion.current().isJava9Compatible()) {
- compileJava.options.release.set(17)
+ compileJava.options.release.set(24)
}
dependencies {
@@ -127,12 +127,12 @@ construo {
targets.configure {
register("linuxX64", Target.Linux) {
architecture.set(Target.Architecture.X86_64)
- jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_linux_hotspot_17.0.15_6.tar.gz")
+ jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/linux/x64/jdk/hotspot/normal/eclipse")
// Linux does not currently have a way to set the icon on the executable
}
register("macM1", Target.MacOs) {
architecture.set(Target.Architecture.AARCH64)
- jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.15_6.tar.gz")
+ jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/mac/aarch64/jdk/hotspot/normal/eclipse")
// macOS needs an identifier
identifier.set("coffee.liz.dyl." + appName)
// Optional: icon for macOS, as an ICNS file
@@ -140,7 +140,7 @@ construo {
}
register("macX64", Target.MacOs) {
architecture.set(Target.Architecture.X86_64)
- jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_mac_hotspot_17.0.15_6.tar.gz")
+ jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/mac/x64/jdk/hotspot/normal/eclipse")
// macOS needs an identifier
identifier.set("coffee.liz.dyl." + appName)
// Optional: icon for macOS, as an ICNS file
@@ -150,7 +150,7 @@ construo {
architecture.set(Target.Architecture.X86_64)
// Optional: icon for Windows, as a PNG
icon.set(project.file("icons/logo.png"))
- jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_windows_hotspot_17.0.15_6.zip")
+ jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/windows/x64/jdk/hotspot/normal/eclipse")
// Uncomment the next line to show a console when the game runs, to print messages.
//useConsole.set(true)
}