Skip to content

Commit

Permalink
feat: add new collections for tracking packet types and listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
Ingrim4 committed May 25, 2024
1 parent b0c4b7f commit ec8c269
Show file tree
Hide file tree
Showing 6 changed files with 676 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.comphenix.protocol.concurrent;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketListener;
import com.google.common.collect.ImmutableSet;

/**
* Manages the association between packet types and their corresponding
* listeners.
* <p>
* This class is thread-safe for modifications. All read methods work on a
* lock-free, best-effort principle, ensuring fast access.
* </p>
*/
public class PacketTypeListenerSet {

// Map to store packet types and their associated listeners
private final Map<PacketType, Set<PacketListener>> typeMap = new HashMap<>();
// Set to store packet classes of packet types that have listeners
private final Set<Class<?>> classSet = new HashSet<>();

/**
* Adds a listener for a specific packet type.
*
* @param packetType the packet type
* @param listener the listener to add
* @return {@code true} if the listener and packetType was added to the set;
* {@code false} otherwise
* @throws NullPointerException if the packetType or listener is null
*/
public synchronized boolean add(PacketType packetType, PacketListener listener) {
Objects.requireNonNull(packetType, "packetType cannot be null");
Objects.requireNonNull(listener, "listener cannot be null");

Set<PacketListener> listenerSet = this.typeMap.computeIfAbsent(packetType, key -> new HashSet<>());
if (!listenerSet.add(listener)) {
return false;
}

// we can always add the packet class here as long as the listener got added
this.classSet.add(packetType.getPacketClass());

return true;
}

/**
* Removes a listener for a specific packet type.
*
* @param packetType the packet type
* @param listener the listener to remove
* @return {@code true} if the set contained the specified listener;
* {@code false} otherwise
* @throws NullPointerException if the packetType or listener is null
*/
public synchronized boolean remove(PacketType packetType, PacketListener listener) {
Objects.requireNonNull(packetType, "packetType cannot be null");
Objects.requireNonNull(listener, "listener cannot be null");

Set<PacketListener> listenerSet = this.typeMap.get(packetType);
if (listenerSet == null) {
// this should never happen so better check for it during unit tests
assert !classSet.contains(packetType.getPacketClass());
return false;
}

if (!listenerSet.remove(listener)) {
// return since the listenerSet didn't change
return false;
}

// packet type has no listeners remove type
if (listenerSet.isEmpty()) {
this.typeMap.remove(packetType);
this.classSet.remove(packetType.getPacketClass());
}

return true;
}

/**
* Checks if there are any listeners for a specific packet type.
*
* @param packetType the packet type
* @return true if there are listeners for the packet type, false otherwise
*/
public boolean contains(PacketType packetType) {
return this.typeMap.containsKey(packetType);
}

/**
* Checks if there are any listeners for a specific packet class.
*
* @param packetClass the packet class
* @return true if there are listeners for the packet class, false otherwise
*/
public boolean contains(Class<?> packetClass) {
return this.classSet.contains(packetClass);
}

/**
* Gets all the packet types that have listeners.
*
* @return a set of packet types that have listeners
*/
public ImmutableSet<PacketType> values() {
return ImmutableSet.copyOf(this.typeMap.keySet());
}

/**
* Clears all listeners and their associated packet types.
*/
public void clear() {
this.typeMap.clear();
this.classSet.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.comphenix.protocol.concurrent;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.ListeningWhitelist;
import com.google.common.collect.ImmutableSet;

/**
* A map-like data structure that associates {@link PacketType}s with sets of
* values. The values are stored in a {@link SortedCopyOnWriteSet}, which
* ensures that the elements are kept in a sorted order based on the
* {@link ListenerPriority} of the {@link ListeningWhitelist}, while maintaining
* their insertion order for elements with equal priorities.
* <p>
* This class is thread-safe for modifications and guarantees a
* modification-free iteration of associated values per packet type. All read methods
* work on a lock-free, best-effort principle, ensuring fast access.
* </p>
*
* @param <T> the type of elements maintained by this map
*/
public class PacketTypeMultiMap<T> {

private final Map<PacketType, SortedCopyOnWriteSet<T, PriorityHolder>> typeMap = new HashMap<>();

/**
* Adds a value to the map, associating it with the {@link PacketType}s
* contained in the specified {@link ListeningWhitelist}. If the value is
* already present in the set (as determined by {@code equals}), it will not be
* added again.
*
* @param key the whitelist containing the packet types to associate the value
* with
* @param value the value to be added
* @throws NullPointerException if the key or value is null
*/
public synchronized void put(ListeningWhitelist key, T value) {
Objects.requireNonNull(key, "key cannot be null");
Objects.requireNonNull(value, "value cannot be null");

for (PacketType packetType : key.getTypes()) {
this.typeMap.computeIfAbsent(packetType, type -> new SortedCopyOnWriteSet<>()).add(value,
new PriorityHolder(key));
}
}

/**
* Removes a value from the map, disassociating it from the {@link PacketType}s
* contained in the specified {@link ListeningWhitelist}. If the value is not
* present, the map remains unchanged.
*
* @param key the whitelist containing the packet types to disassociate the
* value from
* @param value the value to be removed
* @throws NullPointerException if the key or value is null
*/
public synchronized void remove(ListeningWhitelist key, T value) {
Objects.requireNonNull(key, "key cannot be null");
Objects.requireNonNull(value, "value cannot be null");

for (PacketType packetType : key.getTypes()) {
SortedCopyOnWriteSet<T, PriorityHolder> entrySet = this.typeMap.get(packetType);
if (entrySet == null) {
continue;
}

// we shouldn't have empty entrySets
assert !entrySet.isEmpty();

// continue if value wasn't removed
if (!entrySet.remove(value)) {
continue;
}

// remove packet type without entries
if (entrySet.isEmpty()) {
this.typeMap.remove(packetType);
}
}
}

/**
* Returns an immutable set of all {@link PacketType}s currently in the map.
*
* @return an immutable set of packet types
*/
public ImmutableSet<PacketType> getPacketTypes() {
return ImmutableSet.copyOf(this.typeMap.keySet());
}

/**
* Checks if a specified {@link PacketType} is contained in the map.
*
* @param packetType the packet type to check for
* @return {@code true} if the packet type is contained in the map,
* {@code false} otherwise
*/
public boolean contains(PacketType packetType) {
return this.typeMap.containsKey(packetType);
}

/**
* Returns an iterable of values associated with a specified {@link PacketType}.
* If no values are associated with the packet type, an empty iterator is
* returned.
*
* @param packetType the packet type to retrieve values for
* @return an iterable of values associated with the packet type
*/
public Iterable<T> get(PacketType packetType) {
return () -> {
SortedCopyOnWriteSet<T, PriorityHolder> entrySet = this.typeMap.get(packetType);

if (entrySet != null) {
return entrySet.iterator();
}

return Collections.emptyIterator();
};
}

/**
* Clears all entries from the map.
*/
public synchronized void clear() {
this.typeMap.clear();
}

/**
* A holder for priority information used to order elements within the
* {@link SortedCopyOnWriteSet}.
*/
private static class PriorityHolder implements Comparable<PriorityHolder> {

private final ListenerPriority priority;

public PriorityHolder(ListeningWhitelist key) {
this.priority = key.getPriority();
}

@Override
public int compareTo(PriorityHolder other) {
return Integer.compare(this.priority.getSlot(), other.priority.getSlot());
}
}
}
Loading

0 comments on commit ec8c269

Please sign in to comment.