diff --git a/src/main/java/net/minestom/server/event/EventBinding.java b/src/main/java/net/minestom/server/event/EventBinding.java new file mode 100644 index 000000000..8089671dd --- /dev/null +++ b/src/main/java/net/minestom/server/event/EventBinding.java @@ -0,0 +1,67 @@ +package net.minestom.server.event; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +@ApiStatus.Experimental +public interface EventBinding { + + static @NotNull FilteredBuilder filtered(@NotNull EventFilter filter, @NotNull Predicate predicate) { + return new FilteredBuilder<>(filter, predicate); + } + + @NotNull Collection> eventTypes(); + + @NotNull Consumer<@NotNull E> consumer(@NotNull Class eventType); + + class FilteredBuilder { + private final EventFilter filter; + private final Predicate predicate; + private final Map, BiConsumer> mapped = new HashMap<>(); + + FilteredBuilder(EventFilter filter, Predicate predicate) { + this.filter = filter; + this.predicate = predicate; + } + + public FilteredBuilder map(@NotNull Class eventType, + @NotNull BiConsumer<@NotNull T, @NotNull M> consumer) { + //noinspection unchecked + this.mapped.put(eventType, (BiConsumer) consumer); + return this; + } + + public @NotNull EventBinding build() { + final var copy = Map.copyOf(mapped); + final var eventTypes = copy.keySet(); + + Map, Consumer> consumers = new HashMap<>(eventTypes.size()); + for (var eventType : eventTypes) { + final var consumer = copy.get(eventType); + consumers.put(eventType, event -> { + final T handler = filter.getHandler(event); + if (!predicate.test(handler)) return; + consumer.accept(handler, event); + }); + } + return new EventBinding<>() { + @Override + public @NotNull Collection> eventTypes() { + return eventTypes; + } + + @Override + public @NotNull Consumer consumer(@NotNull Class eventType) { + return consumers.get(eventType); + } + }; + } + } +} diff --git a/src/main/java/net/minestom/server/event/EventFilter.java b/src/main/java/net/minestom/server/event/EventFilter.java index 4a7740266..439ae3fd1 100644 --- a/src/main/java/net/minestom/server/event/EventFilter.java +++ b/src/main/java/net/minestom/server/event/EventFilter.java @@ -4,8 +4,10 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.event.trait.*; import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -27,14 +29,16 @@ import java.util.function.Function; */ public interface EventFilter { - EventFilter ALL = from(Event.class, null); - EventFilter ENTITY = from(EntityEvent.class, EntityEvent::getEntity); - EventFilter PLAYER = from(PlayerEvent.class, PlayerEvent::getPlayer); - EventFilter ITEM = from(ItemEvent.class, ItemEvent::getItemStack); - EventFilter INSTANCE = from(InstanceEvent.class, InstanceEvent::getInstance); - EventFilter INVENTORY = from(InventoryEvent.class, InventoryEvent::getInventory); + EventFilter ALL = from(Event.class, null, null); + EventFilter ENTITY = from(EntityEvent.class, Entity.class, EntityEvent::getEntity); + EventFilter PLAYER = from(PlayerEvent.class, Player.class, PlayerEvent::getPlayer); + EventFilter ITEM = from(ItemEvent.class, ItemStack.class, ItemEvent::getItemStack); + EventFilter INSTANCE = from(InstanceEvent.class, Instance.class, InstanceEvent::getInstance); + EventFilter INVENTORY = from(InventoryEvent.class, Inventory.class, InventoryEvent::getInventory); + EventFilter BLOCK = from(BlockEvent.class, Block.class, BlockEvent::getBlock); static EventFilter from(@NotNull Class eventType, + @Nullable Class handlerType, @Nullable Function handlerGetter) { return new EventFilter<>() { @Override @@ -43,9 +47,14 @@ public interface EventFilter { } @Override - public @NotNull Class getEventType() { + public @NotNull Class eventType() { return eventType; } + + @Override + public @Nullable Class handlerType() { + return handlerType; + } }; } @@ -58,10 +67,23 @@ public interface EventFilter { */ @Nullable H getHandler(@NotNull E event); + @ApiStatus.Internal + default @Nullable H castHandler(@NotNull Object event) { + //noinspection unchecked + return getHandler((E) event); + } + /** * The event type to filter on. * * @return The event type. */ - @NotNull Class getEventType(); + @NotNull Class eventType(); + + /** + * The type returned by {@link #getHandler(Event)}. + * + * @return the handler type, null if not any + */ + @Nullable Class handlerType(); } diff --git a/src/main/java/net/minestom/server/event/EventNode.java b/src/main/java/net/minestom/server/event/EventNode.java index c6b359564..83bf2440c 100644 --- a/src/main/java/net/minestom/server/event/EventNode.java +++ b/src/main/java/net/minestom/server/event/EventNode.java @@ -1,19 +1,15 @@ package net.minestom.server.event; -import net.minestom.server.MinecraftServer; import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagReadable; -import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.List; +import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; @@ -27,7 +23,8 @@ import java.util.function.Predicate; * * @param The event type accepted by this node */ -public class EventNode { +@ApiStatus.NonExtendable +public interface EventNode { /** * Creates an event node which accepts any event type with no filtering. @@ -36,7 +33,7 @@ public class EventNode { * @return An event node with no filtering */ @Contract(value = "_ -> new", pure = true) - public static @NotNull EventNode all(@NotNull String name) { + static @NotNull EventNode all(@NotNull String name) { return type(name, EventFilter.ALL); } @@ -55,8 +52,8 @@ public class EventNode { * @return A node with just an event type filter */ @Contract(value = "_, _ -> new", pure = true) - public static @NotNull EventNode type(@NotNull String name, - @NotNull EventFilter filter) { + static @NotNull EventNode type(@NotNull String name, + @NotNull EventFilter filter) { return create(name, filter, null); } @@ -80,9 +77,9 @@ public class EventNode { * @return A node with an event type filter as well as a condition on the event. */ @Contract(value = "_, _, _ -> new", pure = true) - public static @NotNull EventNode event(@NotNull String name, - @NotNull EventFilter filter, - @NotNull Predicate predicate) { + static @NotNull EventNode event(@NotNull String name, + @NotNull EventFilter filter, + @NotNull Predicate predicate) { return create(name, filter, (e, h) -> predicate.test(e)); } @@ -108,9 +105,9 @@ public class EventNode { * @return A node with an event type filter as well as a condition on the event. */ @Contract(value = "_, _, _ -> new", pure = true) - public static @NotNull EventNode type(@NotNull String name, - @NotNull EventFilter filter, - @NotNull BiPredicate predicate) { + static @NotNull EventNode type(@NotNull String name, + @NotNull EventFilter filter, + @NotNull BiPredicate predicate) { return create(name, filter, predicate); } @@ -133,9 +130,9 @@ public class EventNode { * @return A node with an event type filter as well as a condition on the event. */ @Contract(value = "_, _, _ -> new", pure = true) - public static @NotNull EventNode value(@NotNull String name, - @NotNull EventFilter filter, - @NotNull Predicate predicate) { + static @NotNull EventNode value(@NotNull String name, + @NotNull EventFilter filter, + @NotNull Predicate predicate) { return create(name, filter, (e, h) -> predicate.test(h)); } @@ -152,9 +149,9 @@ public class EventNode { * @return A node with an event type filter as well as a handler with the provided tag */ @Contract(value = "_, _, _ -> new", pure = true) - public static @NotNull EventNode tag(@NotNull String name, - @NotNull EventFilter filter, - @NotNull Tag tag) { + static @NotNull EventNode tag(@NotNull String name, + @NotNull EventFilter filter, + @NotNull Tag tag) { return create(name, filter, (e, h) -> h.hasTag(tag)); } @@ -170,57 +167,18 @@ public class EventNode { * @return A node with an event type filter as well as a handler with the provided tag */ @Contract(value = "_, _, _, _ -> new", pure = true) - public static @NotNull EventNode tag(@NotNull String name, - @NotNull EventFilter filter, - @NotNull Tag tag, - @NotNull Predicate<@Nullable V> consumer) { + static @NotNull EventNode tag(@NotNull String name, + @NotNull EventFilter filter, + @NotNull Tag tag, + @NotNull Predicate<@Nullable V> consumer) { return create(name, filter, (e, h) -> consumer.test(h.getTag(tag))); } private static EventNode create(@NotNull String name, @NotNull EventFilter filter, @Nullable BiPredicate predicate) { - return new EventNode<>(name, filter, predicate != null ? (e, o) -> predicate.test(e, (V) o) : null); - } - - private static final Object GLOBAL_CHILD_LOCK = new Object(); - private final Object lock = new Object(); - - private final Map, ListenerEntry> listenerMap = new ConcurrentHashMap<>(); - private final Set> children = new CopyOnWriteArraySet<>(); - - protected final String name; - protected final EventFilter filter; - protected final BiPredicate predicate; - protected final Class eventType; - private volatile int priority; - private volatile EventNode parent; - - protected EventNode(@NotNull String name, - @NotNull EventFilter filter, - @Nullable BiPredicate predicate) { - this.name = name; - this.filter = filter; - this.predicate = predicate; - this.eventType = filter.getEventType(); - } - - /** - * Condition to enter the node. - * - * @param event the called event - * @return true to enter the node, false otherwise - */ - protected boolean condition(@NotNull T event) { - if (predicate == null) - return true; - final var value = filter.getHandler(event); - try { - return predicate.test(event, value); - } catch (Exception e) { - MinecraftServer.getExceptionManager().handleException(e); - return false; - } + //noinspection unchecked + return new EventNodeImpl<>(name, filter, predicate != null ? (e, o) -> predicate.test(e, (V) o) : null); } /** @@ -232,45 +190,7 @@ public class EventNode { * * @param event the event to execute */ - public void call(@NotNull T event) { - final var eventClass = event.getClass(); - if (!eventType.isAssignableFrom(eventClass)) { - // Invalid event type - return; - } - if (!condition(event)) { - // Cancelled by superclass - return; - } - // Process listener list - final var entry = listenerMap.get(eventClass); - if (entry == null) { - // No listener nor children - return; - } - - final var listeners = entry.listeners; - if (!listeners.isEmpty()) { - for (EventListener listener : listeners) { - EventListener.Result result; - try { - result = listener.run(event); - } catch (Exception e) { - result = EventListener.Result.EXCEPTION; - MinecraftServer.getExceptionManager().handleException(e); - } - if (result == EventListener.Result.EXPIRED) { - listeners.remove(listener); - } - } - } - // Process children - if (entry.childCount > 0) { - this.children.stream() - .sorted(Comparator.comparing(EventNode::getPriority)) - .forEach(child -> child.call(event)); - } - } + void call(@NotNull T event); /** * Execute a cancellable event with a callback to execute if the event is successful. @@ -279,7 +199,7 @@ public class EventNode { * @param event The event to execute * @param successCallback A callback if the event is not cancelled */ - public void callCancellable(@NotNull T event, @NotNull Runnable successCallback) { + default void callCancellable(@NotNull T event, @NotNull Runnable successCallback) { call(event); if (!(event instanceof CancellableEvent) || !((CancellableEvent) event).isCancelled()) { successCallback.run(); @@ -287,25 +207,19 @@ public class EventNode { } @Contract(pure = true) - public @NotNull String getName() { - return name; - } + @NotNull Class getEventType(); @Contract(pure = true) - public int getPriority() { - return priority; - } + @NotNull String getName(); + + @Contract(pure = true) + int getPriority(); @Contract(value = "_ -> this") - public @NotNull EventNode setPriority(int priority) { - this.priority = priority; - return this; - } + @NotNull EventNode setPriority(int priority); @Contract(pure = true) - public @Nullable EventNode getParent() { - return parent; - } + @Nullable EventNode getParent(); /** * Returns an unmodifiable view of the children in this node. @@ -314,9 +228,7 @@ public class EventNode { * @see #removeChild(EventNode) */ @Contract(pure = true) - public @NotNull Set<@NotNull EventNode> getChildren() { - return Collections.unmodifiableSet(children); - } + @NotNull Set<@NotNull EventNode> getChildren(); /** * Locates all child nodes with the given name and event type recursively starting at this node. @@ -326,21 +238,7 @@ public class EventNode { * @return All matching event nodes */ @Contract(pure = true) - public @NotNull List> findChildren(@NotNull String name, Class eventType) { - if (children.isEmpty()) { - return Collections.emptyList(); - } - synchronized (GLOBAL_CHILD_LOCK) { - List> result = new ArrayList<>(); - for (EventNode child : children) { - if (EventNode.equals(child, name, eventType)) { - result.add((EventNode) child); - } - result.addAll(child.findChildren(name, eventType)); - } - return result; - } - } + @NotNull List> findChildren(@NotNull String name, Class eventType); /** * Locates all child nodes with the given name and event type recursively starting at this node. @@ -349,8 +247,8 @@ public class EventNode { * @return All matching event nodes */ @Contract(pure = true) - public @NotNull List> findChildren(@NotNull String name) { - return findChildren(name, eventType); + default @NotNull List> findChildren(@NotNull String name) { + return findChildren(name, getEventType()); } /** @@ -362,21 +260,7 @@ public class EventNode { * @param eventType The event node type to filter for * @param eventNode The replacement node */ - public void replaceChildren(@NotNull String name, @NotNull Class eventType, @NotNull EventNode eventNode) { - if (children.isEmpty()) { - return; - } - synchronized (GLOBAL_CHILD_LOCK) { - for (EventNode child : children) { - if (EventNode.equals(child, name, eventType)) { - removeChild(child); - addChild(eventNode); - continue; - } - child.replaceChildren(name, eventType, eventNode); - } - } - } + void replaceChildren(@NotNull String name, @NotNull Class eventType, @NotNull EventNode eventNode); /** * Replaces all children matching the given name and type recursively starting from this node. @@ -386,8 +270,8 @@ public class EventNode { * @param name The node name to filter for * @param eventNode The replacement node */ - public void replaceChildren(@NotNull String name, @NotNull EventNode eventNode) { - replaceChildren(name, eventType, eventNode); + default void replaceChildren(@NotNull String name, @NotNull EventNode eventNode) { + replaceChildren(name, getEventType(), eventNode); } /** @@ -396,29 +280,14 @@ public class EventNode { * @param name The node name to filter for * @param eventType The node type to filter for */ - public void removeChildren(@NotNull String name, @NotNull Class eventType) { - if (children.isEmpty()) { - return; - } - synchronized (GLOBAL_CHILD_LOCK) { - for (EventNode child : children) { - if (EventNode.equals(child, name, eventType)) { - removeChild(child); - continue; - } - child.removeChildren(name, eventType); - } - } - } + void removeChildren(@NotNull String name, @NotNull Class eventType); /** * Recursively removes children with the given name starting at this node. * * @param name The node name to filter for */ - public void removeChildren(@NotNull String name) { - removeChildren(name, eventType); - } + void removeChildren(@NotNull String name); /** * Directly adds a child node to this node. @@ -427,27 +296,7 @@ public class EventNode { * @return this, can be used for chaining */ @Contract(value = "_ -> this") - public @NotNull EventNode addChild(@NotNull EventNode child) { - synchronized (GLOBAL_CHILD_LOCK) { - Check.stateCondition(child.parent != null, "Node already has a parent"); - Check.stateCondition(Objects.equals(parent, child), "Cannot have a child as parent"); - final boolean result = this.children.add((EventNode) child); - if (result) { - child.parent = this; - // Increase listener count - synchronized (lock) { - child.listenerMap.forEach((eventClass, eventListeners) -> { - final var entry = child.listenerMap.get(eventClass); - if (entry == null) - return; - final int childCount = entry.listeners.size() + entry.childCount; - increaseChildListenerCount(eventClass, childCount); - }); - } - } - } - return this; - } + @NotNull EventNode addChild(@NotNull EventNode child); /** * Directly removes the given child from this node. @@ -456,100 +305,28 @@ public class EventNode { * @return this, can be used for chaining */ @Contract(value = "_ -> this") - public @NotNull EventNode removeChild(@NotNull EventNode child) { - synchronized (GLOBAL_CHILD_LOCK) { - final boolean result = this.children.remove(child); - if (result) { - child.parent = null; - // Decrease listener count - synchronized (lock) { - child.listenerMap.forEach((eventClass, eventListeners) -> { - final var entry = child.listenerMap.get(eventClass); - if (entry == null) - return; - final int childCount = entry.listeners.size() + entry.childCount; - decreaseChildListenerCount(eventClass, childCount); - }); - } - } - } - return this; - } + @NotNull EventNode removeChild(@NotNull EventNode child); @Contract(value = "_ -> this") - public @NotNull EventNode addListener(@NotNull EventListener listener) { - synchronized (GLOBAL_CHILD_LOCK) { - final var eventType = listener.getEventType(); - var entry = listenerMap.computeIfAbsent(eventType, aClass -> new ListenerEntry<>()); - entry.listeners.add((EventListener) listener); - if (parent != null) { - synchronized (parent.lock) { - parent.increaseChildListenerCount(eventType, 1); - } - } - } - return this; - } + @NotNull EventNode addListener(@NotNull EventListener listener); @Contract(value = "_, _ -> this") - public @NotNull EventNode addListener(@NotNull Class eventType, @NotNull Consumer<@NotNull E> listener) { + default @NotNull EventNode addListener(@NotNull Class eventType, @NotNull Consumer<@NotNull E> listener) { return addListener(EventListener.of(eventType, listener)); } @Contract(value = "_ -> this") - public @NotNull EventNode removeListener(@NotNull EventListener listener) { - synchronized (GLOBAL_CHILD_LOCK) { - final var eventType = listener.getEventType(); - var entry = listenerMap.get(eventType); - if (entry == null) - return this; - var listeners = entry.listeners; - final boolean removed = listeners.remove(listener); - if (removed && parent != null) { - synchronized (parent.lock) { - parent.decreaseChildListenerCount(eventType, 1); - } - } - } - return this; - } + @NotNull EventNode removeListener(@NotNull EventListener listener); - private void increaseChildListenerCount(Class eventClass, int count) { - var entry = listenerMap.computeIfAbsent(eventClass, aClass -> new ListenerEntry<>()); - ListenerEntry.addAndGet(entry, count); - if (parent != null) { - parent.increaseChildListenerCount(eventClass, count); - } - } + @ApiStatus.Experimental + void map(@NotNull EventNode node, @NotNull Object value); - private void decreaseChildListenerCount(Class eventClass, int count) { - var entry = listenerMap.computeIfAbsent(eventClass, aClass -> new ListenerEntry<>()); - final int result = ListenerEntry.addAndGet(entry, -count); - if (result == 0 && entry.listeners.isEmpty()) { - this.listenerMap.remove(eventClass); - } else if (result < 0) { - throw new IllegalStateException("Something wrong happened, listener count: " + result); - } - if (parent != null) { - parent.decreaseChildListenerCount(eventClass, count); - } - } + @ApiStatus.Experimental + boolean unmap(@NotNull Object value); - private static boolean equals(EventNode node, String name, Class eventType) { - final boolean nameCheck = node.getName().equals(name); - final boolean typeCheck = eventType.isAssignableFrom(node.eventType); - return nameCheck && typeCheck; - } + @ApiStatus.Experimental + void register(@NotNull EventBinding binding); - private static class ListenerEntry { - private static final AtomicIntegerFieldUpdater CHILD_UPDATER = - AtomicIntegerFieldUpdater.newUpdater(ListenerEntry.class, "childCount"); - - List> listeners = new CopyOnWriteArrayList<>(); - volatile int childCount; - - private static int addAndGet(ListenerEntry entry, int add) { - return CHILD_UPDATER.addAndGet(entry, add); - } - } + @ApiStatus.Experimental + void unregister(@NotNull EventBinding binding); } diff --git a/src/main/java/net/minestom/server/event/EventNodeImpl.java b/src/main/java/net/minestom/server/event/EventNodeImpl.java new file mode 100644 index 000000000..8ae2507ec --- /dev/null +++ b/src/main/java/net/minestom/server/event/EventNodeImpl.java @@ -0,0 +1,365 @@ +package net.minestom.server.event; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.IntUnaryOperator; +import java.util.stream.Collectors; + +class EventNodeImpl implements EventNode { + private static final Object GLOBAL_CHILD_LOCK = new Object(); + private final Object lock = new Object(); + + private final Map, ListenerEntry> listenerMap = new ConcurrentHashMap<>(); + private final Set> children = new CopyOnWriteArraySet<>(); + private final Map> mappedNodeCache = new WeakHashMap<>(); + + private final String name; + private final EventFilter filter; + private final BiPredicate predicate; + private final Class eventType; + private volatile int priority; + private volatile EventNodeImpl parent; + + protected EventNodeImpl(@NotNull String name, + @NotNull EventFilter filter, + @Nullable BiPredicate predicate) { + this.name = name; + this.filter = filter; + this.predicate = predicate; + this.eventType = filter.eventType(); + } + + @Override + public void call(@NotNull T event) { + final var eventClass = event.getClass(); + if (!eventType.isAssignableFrom(eventClass)) return; // Invalid event type + // Conditions + if (predicate != null) { + try { + final var value = filter.getHandler(event); + if (!predicate.test(event, value)) return; + } catch (Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + return; + } + } + // Process listeners list + final var entry = listenerMap.get(eventClass); + if (entry == null) return; // No listener nor children + entry.call(event); + // Process children + if (entry.childCount > 0) { + this.children.stream() + .sorted(Comparator.comparing(EventNode::getPriority)) + .forEach(child -> child.call(event)); + } + } + + @Override + public @NotNull List> findChildren(@NotNull String name, Class eventType) { + if (children.isEmpty()) return Collections.emptyList(); + synchronized (GLOBAL_CHILD_LOCK) { + List> result = new ArrayList<>(); + for (EventNode child : children) { + if (equals(child, name, eventType)) { + result.add((EventNode) child); + } + result.addAll(child.findChildren(name, eventType)); + } + return result; + } + } + + @Contract(pure = true) + public @NotNull Set<@NotNull EventNode> getChildren() { + return Collections.unmodifiableSet(children); + } + + @Override + public void replaceChildren(@NotNull String name, @NotNull Class eventType, @NotNull EventNode eventNode) { + synchronized (GLOBAL_CHILD_LOCK) { + if (children.isEmpty()) return; + for (EventNode child : children) { + if (equals(child, name, eventType)) { + removeChild(child); + addChild(eventNode); + continue; + } + child.replaceChildren(name, eventType, eventNode); + } + } + } + + @Override + public void removeChildren(@NotNull String name, @NotNull Class eventType) { + synchronized (GLOBAL_CHILD_LOCK) { + if (children.isEmpty()) return; + for (EventNode child : children) { + if (equals(child, name, eventType)) { + removeChild(child); + continue; + } + child.removeChildren(name, eventType); + } + } + } + + @Override + public void removeChildren(@NotNull String name) { + removeChildren(name, eventType); + } + + @Override + public @NotNull EventNode addChild(@NotNull EventNode child) { + synchronized (GLOBAL_CHILD_LOCK) { + final var childImpl = (EventNodeImpl) child; + Check.stateCondition(childImpl.parent != null, "Node already has a parent"); + Check.stateCondition(Objects.equals(parent, child), "Cannot have a child as parent"); + final boolean result = this.children.add((EventNodeImpl) childImpl); + if (result) { + childImpl.parent = this; + // Increase listener count + propagateNode(childImpl, IntUnaryOperator.identity()); + } + } + return this; + } + + @Override + public @NotNull EventNode removeChild(@NotNull EventNode child) { + synchronized (GLOBAL_CHILD_LOCK) { + final boolean result = this.children.remove(child); + if (result) { + final var childImpl = (EventNodeImpl) child; + childImpl.parent = null; + // Decrease listener count + propagateNode(childImpl, count -> -count); + } + } + return this; + } + + @Override + public @NotNull EventNode addListener(@NotNull EventListener listener) { + synchronized (GLOBAL_CHILD_LOCK) { + final var eventType = listener.getEventType(); + var entry = getEntry(eventType); + entry.listeners.add((EventListener) listener); + propagateToParent(eventType, 1); + } + return this; + } + + @Override + public @NotNull EventNode removeListener(@NotNull EventListener listener) { + synchronized (GLOBAL_CHILD_LOCK) { + final var eventType = listener.getEventType(); + var entry = listenerMap.get(eventType); + if (entry == null) return this; + var listeners = entry.listeners; + final boolean removed = listeners.remove(listener); + if (removed) propagateToParent(eventType, -1); + } + return this; + } + + @Override + public void map(@NotNull EventNode node, @NotNull Object value) { + final var nodeImpl = (EventNodeImpl) node; + final var valueType = value.getClass(); + synchronized (GLOBAL_CHILD_LOCK) { + nodeImpl.listenerMap.forEach((type, listenerEntry) -> { + final var entry = getEntry(type); + final boolean correct = entry.filters.stream().anyMatch(eventFilter -> { + final var handlerType = eventFilter.handlerType(); + return handlerType != null && handlerType.isAssignableFrom(valueType); + }); + Check.stateCondition(!correct, "The node filter {0} is not compatible with type {1}", nodeImpl.eventType, valueType); + synchronized (mappedNodeCache) { + entry.mappedNode.put(value, (EventNode) nodeImpl); + mappedNodeCache.put(value, entry); + // TODO propagate + } + }); + } + } + + @Override + public boolean unmap(@NotNull Object value) { + synchronized (GLOBAL_CHILD_LOCK) { + synchronized (mappedNodeCache) { + var entry = mappedNodeCache.remove(value); + if (entry == null) return false; + final EventNode previousNode = entry.mappedNode.remove(value); + if (previousNode != null) { + // TODO propagate + return true; + } + return false; + } + } + } + + @Override + public void register(@NotNull EventBinding binding) { + synchronized (GLOBAL_CHILD_LOCK) { + for (var eventType : binding.eventTypes()) { + var entry = getEntry((Class) eventType); + final boolean added = entry.bindingConsumers.add((Consumer) binding.consumer(eventType)); + if (added) propagateToParent((Class) eventType, 1); + } + } + } + + @Override + public void unregister(@NotNull EventBinding binding) { + synchronized (GLOBAL_CHILD_LOCK) { + for (var eventType : binding.eventTypes()) { + var entry = listenerMap.get(eventType); + if (entry == null) return; + final boolean removed = entry.bindingConsumers.remove(binding.consumer(eventType)); + if (removed) propagateToParent((Class) eventType, -1); + } + } + } + + @Override + public @NotNull Class getEventType() { + return eventType; + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public @NotNull EventNode setPriority(int priority) { + this.priority = priority; + return this; + } + + @Override + public @Nullable EventNode getParent() { + return parent; + } + + private void propagateChildCountChange(Class eventClass, int count) { + var entry = getEntry(eventClass); + final int result = ListenerEntry.CHILD_UPDATER.addAndGet(entry, count); + if (result == 0 && entry.listeners.isEmpty()) { + this.listenerMap.remove(eventClass); + } else if (result < 0) { + throw new IllegalStateException("Something wrong happened, listener count: " + result); + } + if (parent != null) { + parent.propagateChildCountChange(eventClass, count); + } + } + + private void propagateToParent(Class eventClass, int count) { + final var parent = this.parent; + if (parent != null) { + synchronized (parent.lock) { + parent.propagateChildCountChange(eventClass, count); + } + } + } + + private void propagateNode(EventNodeImpl child, IntUnaryOperator operator) { + synchronized (lock) { + final var listeners = child.listenerMap; + listeners.forEach((eventClass, eventListeners) -> { + final var entry = listeners.get(eventClass); + if (entry == null) return; + final int childCount = entry.listeners.size() + entry.childCount; + propagateChildCountChange(eventClass, operator.applyAsInt(childCount)); + }); + } + } + + private ListenerEntry getEntry(Class type) { + return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>(this, (Class) aClass)); + } + + private static boolean equals(EventNode node, String name, Class eventType) { + final boolean nameCheck = node.getName().equals(name); + final boolean typeCheck = eventType.isAssignableFrom(((EventNodeImpl) node).eventType); + return nameCheck && typeCheck; + } + + private static class ListenerEntry { + private static final List> FILTERS = List.of( + EventFilter.ENTITY, + EventFilter.ITEM, EventFilter.INSTANCE, + EventFilter.INVENTORY, EventFilter.BLOCK); + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater CHILD_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(ListenerEntry.class, "childCount"); + + final EventNodeImpl node; + final List> filters; + final List> listeners = new CopyOnWriteArrayList<>(); + final Set> bindingConsumers = new CopyOnWriteArraySet<>(); + final Map> mappedNode = new WeakHashMap<>(); + volatile int childCount; + + ListenerEntry(EventNodeImpl node, Class eventType) { + this.node = node; + this.filters = FILTERS.stream().filter(eventFilter -> eventFilter.eventType().isAssignableFrom(eventType)).collect(Collectors.toList()); + } + + void call(T event) { + // Event interfaces + if (!bindingConsumers.isEmpty()) { + for (var consumer : bindingConsumers) { + consumer.accept(event); + } + } + // Mapped listeners + if (!mappedNode.isEmpty()) { + synchronized (node.mappedNodeCache) { + if (!mappedNode.isEmpty()) { + // Check mapped listeners for each individual event handler + for (var filter : filters) { + final var handler = filter.castHandler(event); + final var map = mappedNode.get(handler); + if (map != null) map.call(event); + } + } + } + } + // Basic listeners + if (!listeners.isEmpty()) { + for (EventListener listener : listeners) { + EventListener.Result result; + try { + result = listener.run(event); + } catch (Exception e) { + result = EventListener.Result.EXCEPTION; + MinecraftServer.getExceptionManager().handleException(e); + } + if (result == EventListener.Result.EXPIRED) { + listeners.remove(listener); + } + } + } + } + } +} diff --git a/src/main/java/net/minestom/server/event/GlobalEventHandler.java b/src/main/java/net/minestom/server/event/GlobalEventHandler.java index 0e1a8c94f..b97f42cc3 100644 --- a/src/main/java/net/minestom/server/event/GlobalEventHandler.java +++ b/src/main/java/net/minestom/server/event/GlobalEventHandler.java @@ -7,7 +7,7 @@ import java.util.function.Consumer; /** * Object containing all the global event listeners. */ -public final class GlobalEventHandler extends EventNode { +public final class GlobalEventHandler extends EventNodeImpl { public GlobalEventHandler() { super("global", EventFilter.ALL, null); } diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 1aa1a322a..7001f3e66 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -2,6 +2,7 @@ package net.minestom.server.instance; import com.extollit.gaming.ai.path.model.ColumnarOcclusionFieldList; import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; @@ -88,20 +89,18 @@ public class DynamicChunk extends Chunk { @Override public void tick(long time) { - if (tickableMap.isEmpty()) - return; - for (var entry : tickableMap.int2ObjectEntrySet()) { + if (tickableMap.isEmpty()) return; + Int2ObjectMaps.fastForEach(tickableMap, entry -> { final int index = entry.getIntKey(); final Block block = entry.getValue(); - final var handler = block.handler(); - if (handler != null) { - final int x = ChunkUtils.blockIndexToChunkPositionX(index); - final int y = ChunkUtils.blockIndexToChunkPositionY(index); - final int z = ChunkUtils.blockIndexToChunkPositionZ(index); - final Vec blockPosition = new Vec(x, y, z); - handler.tick(new BlockHandler.Tick(block, instance, blockPosition)); - } - } + final BlockHandler handler = block.handler(); + if (handler == null) return; + final int x = ChunkUtils.blockIndexToChunkPositionX(index); + final int y = ChunkUtils.blockIndexToChunkPositionY(index); + final int z = ChunkUtils.blockIndexToChunkPositionZ(index); + final Vec blockPosition = new Vec(x, y, z); + handler.tick(new BlockHandler.Tick(block, instance, blockPosition)); + }); } @Override diff --git a/src/main/java/net/minestom/server/instance/block/BlockImpl.java b/src/main/java/net/minestom/server/instance/block/BlockImpl.java index d9be4d089..df311d2f4 100644 --- a/src/main/java/net/minestom/server/instance/block/BlockImpl.java +++ b/src/main/java/net/minestom/server/instance/block/BlockImpl.java @@ -69,6 +69,8 @@ final class BlockImpl implements Block { private final NBTCompound nbt; private final BlockHandler handler; + private int hashCode; // Cache + BlockImpl(@NotNull Registry.BlockEntry registry, @NotNull Map, Block> propertyEntry, @NotNull Map properties, @@ -151,7 +153,12 @@ final class BlockImpl implements Block { @Override public int hashCode() { - return Objects.hash(stateId(), nbt, handler); + int result = hashCode; + if (result == 0) { + result = Objects.hash(stateId(), nbt, handler); + this.hashCode = result; + } + return result; } @Override