package net.minestom.server.event; import com.github.benmanes.caffeine.cache.Caffeine; import net.minestom.server.MinecraftServer; import net.minestom.server.event.trait.RecursiveEvent; 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.function.BiConsumer; import java.util.function.BiPredicate; import java.util.function.Consumer; non-sealed class EventNodeImpl implements EventNode { static final Object GLOBAL_CHILD_LOCK = new Object(); private final Map> handleMap = new ConcurrentHashMap<>(); final Map, ListenerEntry> listenerMap = new ConcurrentHashMap<>(); final Set> children = Collections.newSetFromMap(Caffeine.newBuilder() .weakKeys()., Boolean>build().asMap()); final Map> mappedNodeCache = Caffeine.newBuilder() .weakKeys().weakValues().>build().asMap(); final Map> registeredMappedNode = Caffeine.newBuilder() .weakKeys().weakValues().>build().asMap(); final String name; final EventFilter filter; final BiPredicate predicate; final Class eventType; volatile int priority; volatile EventNodeImpl parent; EventNodeImpl(@NotNull String name, @NotNull EventFilter filter, @Nullable BiPredicate predicate) { this.name = name; this.filter = filter; this.predicate = predicate; this.eventType = filter.eventType(); } @Override @SuppressWarnings("unchecked") public @NotNull ListenerHandle getHandle(@NotNull Class handleType) { return (ListenerHandle) handleMap.computeIfAbsent(handleType, aClass -> new Handle<>((Class) aClass)); } @Override public @NotNull List> findChildren(@NotNull String name, Class eventType) { synchronized (GLOBAL_CHILD_LOCK) { final Set> children = getChildren(); if (children.isEmpty()) return List.of(); 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) { final Set> children = getChildren(); if (children.isEmpty()) return; for (EventNode child : children) { if (equals(child, name, eventType)) { removeChild(child); addChild(eventNode); break; } child.replaceChildren(name, eventType, eventNode); } } } @Override public void removeChildren(@NotNull String name, @NotNull Class eventType) { synchronized (GLOBAL_CHILD_LOCK) { final Set> children = getChildren(); if (children.isEmpty()) return; for (EventNode child : children) { if (equals(child, name, eventType)) { removeChild(child); continue; } child.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"); if (!children.add((EventNodeImpl) childImpl)) return this; // Couldn't add the child (already present?) childImpl.parent = this; childImpl.invalidateEventsFor(this); } return this; } @Override public @NotNull EventNode removeChild(@NotNull EventNode child) { synchronized (GLOBAL_CHILD_LOCK) { final var childImpl = (EventNodeImpl) child; final boolean result = this.children.remove(childImpl); if (!result) return this; // Child not found childImpl.parent = null; childImpl.invalidateEventsFor(this); } return this; } @Override public @NotNull EventNode addListener(@NotNull EventListener listener) { synchronized (GLOBAL_CHILD_LOCK) { final var eventType = listener.eventType(); ListenerEntry entry = getEntry(eventType); entry.listeners.add((EventListener) listener); invalidateEvent(eventType); } return this; } @Override public @NotNull EventNode removeListener(@NotNull EventListener listener) { synchronized (GLOBAL_CHILD_LOCK) { final var eventType = listener.eventType(); ListenerEntry entry = listenerMap.get(eventType); if (entry == null) return this; // There is no listener with such type if (entry.listeners.remove(listener)) invalidateEvent(eventType); } return this; } @Override public @NotNull EventNode map(@NotNull H value, @NotNull EventFilter filter) { EventNodeImpl node; synchronized (GLOBAL_CHILD_LOCK) { node = new EventNodeLazyImpl<>(this, value, filter); Check.stateCondition(node.parent != null, "Node already has a parent"); Check.stateCondition(Objects.equals(parent, node), "Cannot map to self"); EventNodeImpl previous = this.mappedNodeCache.putIfAbsent(value, (EventNodeImpl) node); if (previous != null) return (EventNode) previous; node.parent = this; } return node; } @Override public void unmap(@NotNull Object value) { synchronized (GLOBAL_CHILD_LOCK) { final var mappedNode = this.registeredMappedNode.remove(value); if (mappedNode != null) mappedNode.invalidateEventsFor(this); } } @Override public void register(@NotNull EventBinding binding) { synchronized (GLOBAL_CHILD_LOCK) { for (var eventType : binding.eventTypes()) { ListenerEntry entry = getEntry((Class) eventType); final boolean added = entry.bindingConsumers.add((Consumer) binding.consumer(eventType)); if (added) invalidateEvent((Class) eventType); } } } @Override public void unregister(@NotNull EventBinding binding) { synchronized (GLOBAL_CHILD_LOCK) { for (var eventType : binding.eventTypes()) { ListenerEntry entry = listenerMap.get(eventType); if (entry == null) return; final boolean removed = entry.bindingConsumers.remove(binding.consumer(eventType)); if (removed) invalidateEvent((Class) eventType); } } } @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; } @Override public String toString() { return createStringGraph(createGraph()); } Graph createGraph() { synchronized (GLOBAL_CHILD_LOCK) { List children = this.children.stream().map(EventNodeImpl::createGraph).toList(); return new Graph(getName(), getEventType().getSimpleName(), getPriority(), children); } } static String createStringGraph(Graph graph) { StringBuilder buffer = new StringBuilder(); genToStringTree(buffer, "", "", graph); return buffer.toString(); } private static void genToStringTree(StringBuilder buffer, String prefix, String childrenPrefix, Graph graph) { buffer.append(prefix); buffer.append(String.format("%s - EventType: %s - Priority: %d", graph.name(), graph.eventType(), graph.priority())); buffer.append('\n'); var nextNodes = graph.children(); for (Iterator iterator = nextNodes.iterator(); iterator.hasNext(); ) { Graph next = iterator.next(); if (iterator.hasNext()) { genToStringTree(buffer, childrenPrefix + '├' + '─' + " ", childrenPrefix + '│' + " ", next); } else { genToStringTree(buffer, childrenPrefix + '└' + '─' + " ", childrenPrefix + " ", next); } } } record Graph(String name, String eventType, int priority, List children) { public Graph { children = children.stream().sorted(Comparator.comparingInt(Graph::priority)).toList(); } } void invalidateEventsFor(EventNodeImpl node) { assert Thread.holdsLock(GLOBAL_CHILD_LOCK); for (Class eventType : listenerMap.keySet()) { node.invalidateEvent(eventType); } // TODO bindings? for (EventNodeImpl child : children) { child.invalidateEventsFor(node); } } private void invalidateEvent(Class eventClass) { forTargetEvents(eventClass, type -> { Handle handle = handleMap.computeIfAbsent(type, aClass -> new Handle<>((Class) aClass)); handle.invalidate(); }); final EventNodeImpl parent = this.parent; if (parent != null) parent.invalidateEvent(eventClass); } private ListenerEntry getEntry(Class type) { return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>()); } private static boolean equals(EventNode node, String name, Class eventType) { return node.getName().equals(name) && eventType.isAssignableFrom((node.getEventType())); } private static void forTargetEvents(Class type, Consumer> consumer) { consumer.accept(type); // Recursion if (RecursiveEvent.class.isAssignableFrom(type)) { final Class superclass = type.getSuperclass(); if (superclass != null && RecursiveEvent.class.isAssignableFrom(superclass)) { forTargetEvents(superclass, consumer); } } } private static class ListenerEntry { final List> listeners = new CopyOnWriteArrayList<>(); final Set> bindingConsumers = new CopyOnWriteArraySet<>(); } @SuppressWarnings("unchecked") final class Handle implements ListenerHandle { private final Class eventType; private Consumer listener = null; private volatile boolean updated; Handle(Class eventType) { this.eventType = eventType; } @Override public void call(@NotNull E event) { final Consumer listener = updatedListener(); if (listener == null) return; try { listener.accept(event); } catch (Throwable e) { MinecraftServer.getExceptionManager().handleException(e); } } @Override public boolean hasListener() { return updatedListener() != null; } void invalidate() { this.updated = false; } @Nullable Consumer updatedListener() { if (updated) return listener; synchronized (GLOBAL_CHILD_LOCK) { if (updated) return listener; final Consumer listener = createConsumer(); this.listener = listener; this.updated = true; return listener; } } private @Nullable Consumer createConsumer() { var node = (EventNodeImpl) EventNodeImpl.this; // Standalone listeners List> listeners = new ArrayList<>(); forTargetEvents(eventType, type -> { final ListenerEntry entry = node.listenerMap.get(type); if (entry != null) { final Consumer result = listenersConsumer(entry); if (result != null) listeners.add(result); } }); final Consumer[] listenersArray = listeners.toArray(Consumer[]::new); // Mapped final Consumer mappedListener = mappedConsumer(); // Children final Consumer[] childrenListeners = node.children.stream() .filter(child -> child.eventType.isAssignableFrom(eventType)) // Invalid event type .sorted(Comparator.comparing(EventNode::getPriority)) .map(child -> ((Handle) child.getHandle(eventType)).updatedListener()) .filter(Objects::nonNull) .toArray(Consumer[]::new); // Empty check final BiPredicate predicate = node.predicate; final EventFilter filter = node.filter; final boolean hasPredicate = predicate != null; final boolean hasListeners = listenersArray.length > 0; final boolean hasMap = mappedListener != null; final boolean hasChildren = childrenListeners.length > 0; if (!hasListeners && !hasMap && !hasChildren) { // No listener return null; } return e -> { // Filtering if (hasPredicate) { final Object value = filter.getHandler(e); if (!predicate.test(e, value)) return; } // Normal listeners if (hasListeners) { for (Consumer listener : listenersArray) { listener.accept(e); } } // Mapped nodes if (hasMap) mappedListener.accept(e); // Children if (hasChildren) { for (Consumer childHandle : childrenListeners) { childHandle.accept(e); } } }; } /** * Create a consumer calling all listeners from {@link EventNode#addListener(EventListener)} and * {@link EventNode#register(EventBinding)}. *

* Most computation should ideally be done outside the consumers as a one-time cost. */ private @Nullable Consumer listenersConsumer(@NotNull ListenerEntry entry) { final EventListener[] listenersCopy = entry.listeners.toArray(EventListener[]::new); final Consumer[] bindingsCopy = entry.bindingConsumers.toArray(Consumer[]::new); final boolean listenersEmpty = listenersCopy.length == 0; final boolean bindingsEmpty = bindingsCopy.length == 0; if (listenersEmpty && bindingsEmpty) return null; if (bindingsEmpty && listenersCopy.length == 1) { // Only one normal listener final EventListener listener = listenersCopy[0]; return e -> callListener(listener, e); } // Worse case scenario, try to run everything return e -> { if (!listenersEmpty) { for (EventListener listener : listenersCopy) { callListener(listener, e); } } if (!bindingsEmpty) { for (Consumer eConsumer : bindingsCopy) { eConsumer.accept(e); } } }; } /** * Create a consumer handling {@link EventNode#map(Object, EventFilter)}. * The goal is to limit the amount of map lookup. */ private @Nullable Consumer mappedConsumer() { var node = (EventNodeImpl) EventNodeImpl.this; final var mappedNodeCache = node.registeredMappedNode; if (mappedNodeCache.isEmpty()) return null; Set> filters = new HashSet<>(mappedNodeCache.size()); Map> handlers = new WeakHashMap<>(mappedNodeCache.size()); // Retrieve all filters used to retrieve potential handlers for (var mappedEntry : mappedNodeCache.entrySet()) { final EventNodeImpl mappedNode = mappedEntry.getValue(); final Handle handle = (Handle) mappedNode.getHandle(eventType); if (!handle.hasListener()) continue; // Implicit update filters.add(mappedNode.filter); handlers.put(mappedEntry.getKey(), handle); } // If at least one mapped node listen to this handle type, // loop through them and forward to mapped node if there is a match if (filters.isEmpty()) return null; final EventFilter[] filterList = filters.toArray(EventFilter[]::new); final BiConsumer, E> mapper = (filter, event) -> { final Object handler = filter.castHandler(event); final Handle handle = handlers.get(handler); if (handle != null) handle.call(event); }; // Specialize the consumer depending on the number of filters to avoid looping return switch (filterList.length) { case 1 -> event -> mapper.accept(filterList[0], event); case 2 -> event -> { mapper.accept(filterList[0], event); mapper.accept(filterList[1], event); }; case 3 -> event -> { mapper.accept(filterList[0], event); mapper.accept(filterList[1], event); mapper.accept(filterList[2], event); }; default -> event -> { for (var filter : filterList) { mapper.accept(filter, event); } }; }; } void callListener(@NotNull EventListener listener, E event) { var node = (EventNodeImpl) EventNodeImpl.this; EventListener.Result result = listener.run(event); if (result == EventListener.Result.EXPIRED) { node.removeListener(listener); invalidate(); } } } }