diff --git a/.github/workflows/trigger-jitpack-build.yml b/.github/workflows/trigger-jitpack-build.yml new file mode 100644 index 000000000..8366c4f6f --- /dev/null +++ b/.github/workflows/trigger-jitpack-build.yml @@ -0,0 +1,12 @@ +name: Trigger Jitpack Build +on: + push: + branches: [ master ] + + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Trigger Jitpack Build + run: curl "https://jitpack.io/com/github/Minestom/Minestom/${GITHUB_SHA:0:10}/build.log" diff --git a/src/lwjgl/java/net/minestom/server/map/framebuffers/GLFWCapableBuffer.java b/src/lwjgl/java/net/minestom/server/map/framebuffers/GLFWCapableBuffer.java index 92dc949f0..6afe6c960 100644 --- a/src/lwjgl/java/net/minestom/server/map/framebuffers/GLFWCapableBuffer.java +++ b/src/lwjgl/java/net/minestom/server/map/framebuffers/GLFWCapableBuffer.java @@ -4,6 +4,7 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.map.Framebuffer; import net.minestom.server.map.MapColors; import net.minestom.server.timer.Task; +import net.minestom.server.utils.thread.ThreadBindingExecutor; import org.lwjgl.BufferUtils; import org.lwjgl.PointerBuffer; import org.lwjgl.glfw.GLFWErrorCallback; @@ -27,6 +28,8 @@ public abstract class GLFWCapableBuffer { private final ByteBuffer colorsBuffer; private boolean onlyMapColors; + private static ThreadBindingExecutor threadBindingPool; + protected GLFWCapableBuffer(int width, int height) { this(width, height, GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API); } @@ -60,6 +63,12 @@ public abstract class GLFWCapableBuffer { throw new RuntimeException("("+errcode+") Failed to create GLFW Window."); } } + + synchronized(GLFWCapableBuffer.class) { + if(threadBindingPool == null) { + threadBindingPool = new ThreadBindingExecutor(MinecraftServer.THREAD_COUNT_SCHEDULER, MinecraftServer.THREAD_NAME_SCHEDULER); + } + } } public GLFWCapableBuffer unbindContextFromThread() { @@ -80,14 +89,17 @@ public abstract class GLFWCapableBuffer { return MinecraftServer.getSchedulerManager() .buildTask(new Runnable() { private boolean first = true; - - @Override - public void run() { + private final Runnable subAction = () -> { if(first) { changeRenderingThreadToCurrent(); first = false; } render(rendering); + }; + + @Override + public void run() { + threadBindingPool.execute(subAction); } }) .repeat(period) diff --git a/src/main/java/net/minestom/server/attribute/AttributeModifier.java b/src/main/java/net/minestom/server/attribute/AttributeModifier.java index 3cdd9be0f..2bcb99e08 100644 --- a/src/main/java/net/minestom/server/attribute/AttributeModifier.java +++ b/src/main/java/net/minestom/server/attribute/AttributeModifier.java @@ -1,10 +1,8 @@ package net.minestom.server.attribute; -import net.minestom.server.utils.UniqueIdUtils; import org.jetbrains.annotations.NotNull; import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; /** * Represent an attribute modifier. @@ -24,7 +22,7 @@ public class AttributeModifier { * @param operation the operation to apply this modifier with */ public AttributeModifier(@NotNull String name, float amount, @NotNull AttributeOperation operation) { - this(UniqueIdUtils.createRandomUUID(ThreadLocalRandom.current()), name, amount, operation); + this(UUID.randomUUID(), name, amount, operation); } /** diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index 447d37430..7aca98339 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -7,7 +7,9 @@ import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; /** @@ -18,13 +20,42 @@ public class BoundingBox { private final Entity entity; private final double x, y, z; - private volatile Pos lastPosition; - private List bottomFace; - private List topFace; - private List leftFace; - private List rightFace; - private List frontFace; - private List backFace; + private final CachedFace bottomFace = new CachedFace(() -> List.of( + new Vec(getMinX(), getMinY(), getMinZ()), + new Vec(getMaxX(), getMinY(), getMinZ()), + new Vec(getMaxX(), getMinY(), getMaxZ()), + new Vec(getMinX(), getMinY(), getMaxZ()) + )); + private final CachedFace topFace = new CachedFace(() -> List.of( + new Vec(getMinX(), getMaxY(), getMinZ()), + new Vec(getMaxX(), getMaxY(), getMinZ()), + new Vec(getMaxX(), getMaxY(), getMaxZ()), + new Vec(getMinX(), getMaxY(), getMaxZ()) + )); + private final CachedFace leftFace = new CachedFace(() -> List.of( + new Vec(getMinX(), getMinY(), getMinZ()), + new Vec(getMinX(), getMaxY(), getMinZ()), + new Vec(getMinX(), getMaxY(), getMaxZ()), + new Vec(getMinX(), getMinY(), getMaxZ()) + )); + private final CachedFace rightFace = new CachedFace(() -> List.of( + new Vec(getMaxX(), getMinY(), getMinZ()), + new Vec(getMaxX(), getMaxY(), getMinZ()), + new Vec(getMaxX(), getMaxY(), getMaxZ()), + new Vec(getMaxX(), getMinY(), getMaxZ()) + )); + private final CachedFace frontFace = new CachedFace(() -> List.of( + new Vec(getMinX(), getMinY(), getMinZ()), + new Vec(getMaxX(), getMinY(), getMinZ()), + new Vec(getMaxX(), getMaxY(), getMinZ()), + new Vec(getMinX(), getMaxY(), getMinZ()) + )); + private final CachedFace backFace = new CachedFace(() -> List.of( + new Vec(getMinX(), getMinY(), getMaxZ()), + new Vec(getMaxX(), getMinY(), getMaxZ()), + new Vec(getMaxX(), getMaxY(), getMaxZ()), + new Vec(getMinX(), getMaxY(), getMaxZ()) + )); /** * Creates a {@link BoundingBox} linked to an {@link Entity} and with a specific size. @@ -320,12 +351,7 @@ public class BoundingBox { * @return the points at the bottom of the {@link BoundingBox} */ public @NotNull List getBottomFace() { - this.bottomFace = get(bottomFace, () -> - List.of(new Vec(getMinX(), getMinY(), getMinZ()), - new Vec(getMaxX(), getMinY(), getMinZ()), - new Vec(getMaxX(), getMinY(), getMaxZ()), - new Vec(getMinX(), getMinY(), getMaxZ()))); - return bottomFace; + return bottomFace.get(); } /** @@ -334,12 +360,7 @@ public class BoundingBox { * @return the points at the top of the {@link BoundingBox} */ public @NotNull List getTopFace() { - this.topFace = get(topFace, () -> - List.of(new Vec(getMinX(), getMaxY(), getMinZ()), - new Vec(getMaxX(), getMaxY(), getMinZ()), - new Vec(getMaxX(), getMaxY(), getMaxZ()), - new Vec(getMinX(), getMaxY(), getMaxZ()))); - return topFace; + return topFace.get(); } /** @@ -348,12 +369,7 @@ public class BoundingBox { * @return the points on the left face of the {@link BoundingBox} */ public @NotNull List getLeftFace() { - this.leftFace = get(leftFace, () -> - List.of(new Vec(getMinX(), getMinY(), getMinZ()), - new Vec(getMinX(), getMaxY(), getMinZ()), - new Vec(getMinX(), getMaxY(), getMaxZ()), - new Vec(getMinX(), getMinY(), getMaxZ()))); - return leftFace; + return leftFace.get(); } /** @@ -362,12 +378,7 @@ public class BoundingBox { * @return the points on the right face of the {@link BoundingBox} */ public @NotNull List getRightFace() { - this.rightFace = get(rightFace, () -> - List.of(new Vec(getMaxX(), getMinY(), getMinZ()), - new Vec(getMaxX(), getMaxY(), getMinZ()), - new Vec(getMaxX(), getMaxY(), getMaxZ()), - new Vec(getMaxX(), getMinY(), getMaxZ()))); - return rightFace; + return rightFace.get(); } /** @@ -376,12 +387,7 @@ public class BoundingBox { * @return the points at the front of the {@link BoundingBox} */ public @NotNull List getFrontFace() { - this.frontFace = get(frontFace, () -> - List.of(new Vec(getMinX(), getMinY(), getMinZ()), - new Vec(getMaxX(), getMinY(), getMinZ()), - new Vec(getMaxX(), getMaxY(), getMinZ()), - new Vec(getMinX(), getMaxY(), getMinZ()))); - return frontFace; + return frontFace.get(); } /** @@ -390,12 +396,7 @@ public class BoundingBox { * @return the points at the back of the {@link BoundingBox} */ public @NotNull List getBackFace() { - this.backFace = get(backFace, () -> List.of( - new Vec(getMinX(), getMinY(), getMaxZ()), - new Vec(getMaxX(), getMinY(), getMaxZ()), - new Vec(getMaxX(), getMaxY(), getMaxZ()), - new Vec(getMinX(), getMaxY(), getMaxZ()))); - return backFace; + return backFace.get(); } @Override @@ -410,18 +411,42 @@ public class BoundingBox { return result; } - private @NotNull List get(@Nullable List face, Supplier> vecSupplier) { - final var lastPos = this.lastPosition; - final var entityPos = entity.getPosition(); - if (face != null && lastPos != null && lastPos.samePoint(entityPos)) { - return face; - } - this.lastPosition = entityPos; - return vecSupplier.get(); - } - private enum Axis { X, Y, Z } + private class CachedFace { + + private final AtomicReference<@Nullable PositionedPoints> reference = new AtomicReference<>(null); + private final Supplier<@NotNull List> faceProducer; + + private CachedFace(Supplier<@NotNull List> faceProducer) { + this.faceProducer = faceProducer; + } + + @NotNull List get() { + //noinspection ConstantConditions + return reference.updateAndGet(value -> { + Pos entityPosition = entity.getPosition(); + if (value == null || !value.lastPosition.samePoint(entityPosition)) { + return new PositionedPoints(entityPosition, faceProducer.get()); + } + return value; + }).points; + } + + } + + private static class PositionedPoints { + + private final @NotNull Pos lastPosition; + private final @NotNull List points; + + private PositionedPoints(@NotNull Pos lastPosition, @NotNull List points) { + this.lastPosition = lastPosition; + this.points = points; + } + + } + } diff --git a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/registry/ArgumentEntityType.java b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/registry/ArgumentEntityType.java index def764fc1..6e2a2749d 100644 --- a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/registry/ArgumentEntityType.java +++ b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/registry/ArgumentEntityType.java @@ -23,7 +23,7 @@ public class ArgumentEntityType extends ArgumentRegistry { @Override public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) { DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, true); - argumentNode.parser = "minecraft:entity_summon"; + argumentNode.parser = "minecraft:resource_location"; argumentNode.suggestionsType = SuggestionType.SUMMONABLE_ENTITIES.getIdentifier(); nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode}); diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 36a7833c5..2cd62ed74 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -450,7 +450,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler update(time); ticks++; - EventDispatcher.call(new EntityTickEvent(this), GlobalHandles.ENTITY_TICK); + GlobalHandles.ENTITY_TICK.call(new EntityTickEvent(this)); // remove expired effects effectTick(time); @@ -883,16 +883,15 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler public void addPassenger(@NotNull Entity entity) { Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance"); Check.stateCondition(entity == getVehicle(), "Cannot add the entity vehicle as a passenger"); - final Entity vehicle = entity.getVehicle(); if (vehicle != null) { vehicle.removePassenger(entity); } - this.passengers.add(entity); entity.vehicle = this; - sendPacketToViewersAndSelf(getPassengersPacket()); + entity.refreshPosition(position); + entity.synchronizePosition(true); } /** @@ -904,11 +903,14 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler */ public void removePassenger(@NotNull Entity entity) { Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance"); - - if (!passengers.remove(entity)) - return; + if (!passengers.remove(entity)) return; entity.vehicle = null; sendPacketToViewersAndSelf(getPassengersPacket()); + if (entity instanceof Player) { + Player player = (Player) entity; + player.getPlayerConnection().sendPacket(new PlayerPositionAndLookPacket(player.getPosition(), + (byte) 0x00, player.getNextTeleportId(), true)); + } } /** diff --git a/src/main/java/net/minestom/server/entity/EntityTypeImpl.java b/src/main/java/net/minestom/server/entity/EntityTypeImpl.java index 3d89f8f1c..50c6eb833 100644 --- a/src/main/java/net/minestom/server/entity/EntityTypeImpl.java +++ b/src/main/java/net/minestom/server/entity/EntityTypeImpl.java @@ -23,6 +23,7 @@ import net.minestom.server.entity.metadata.monster.skeleton.SkeletonMeta; import net.minestom.server.entity.metadata.monster.skeleton.StrayMeta; import net.minestom.server.entity.metadata.monster.skeleton.WitherSkeletonMeta; import net.minestom.server.entity.metadata.monster.zombie.DrownedMeta; +import net.minestom.server.entity.metadata.monster.zombie.HuskMeta; import net.minestom.server.entity.metadata.monster.zombie.ZombieMeta; import net.minestom.server.entity.metadata.monster.zombie.ZombieVillagerMeta; import net.minestom.server.entity.metadata.monster.zombie.ZombifiedPiglinMeta; @@ -119,6 +120,7 @@ final class EntityTypeImpl implements EntityType { supplier.put("minecraft:guardian", GuardianMeta::new); supplier.put("minecraft:hoglin", HoglinMeta::new); supplier.put("minecraft:horse", HorseMeta::new); + supplier.put("minecraft:husk", HuskMeta::new); supplier.put("minecraft:illusioner", IllusionerMeta::new); supplier.put("minecraft:iron_golem", IronGolemMeta::new); supplier.put("minecraft:item", ItemEntityMeta::new); diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index a3070ceed..dd32d7506 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -351,7 +351,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } // Tick event - EventDispatcher.call(new PlayerTickEvent(this), GlobalHandles.PLAYER_TICK); + GlobalHandles.PLAYER_TICK.call(new PlayerTickEvent(this)); } @Override @@ -418,6 +418,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, getPlayerConnection().sendPacket(respawnPacket); PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this); EventDispatcher.call(respawnEvent); + triggerStatus((byte) (24 + permissionLevel)); // Set permission level refreshIsDead(false); // Runnable called when teleportation is successful (after loading and sending necessary chunk) @@ -1195,7 +1196,11 @@ public class Player extends LivingEntity implements CommandSender, Localizable, // Cannot load chunk (auto load is not enabled) return; } - chunk.addViewer(this); + try { + chunk.addViewer(this); + } catch (Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + } }); }); } @@ -1548,6 +1553,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable, playerConnection.sendPacket(new UpdateViewPositionPacket(chunkX, chunkZ)); } + public int getNextTeleportId() { + return teleportId.incrementAndGet(); + } + public int getLastSentTeleportId() { return teleportId.get(); } @@ -1567,7 +1576,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, @ApiStatus.Internal protected void synchronizePosition(boolean includeSelf) { if (includeSelf) { - playerConnection.sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, teleportId.incrementAndGet(), false)); + playerConnection.sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, getNextTeleportId(), false)); } super.synchronizePosition(includeSelf); } diff --git a/src/main/java/net/minestom/server/event/EventDispatcher.java b/src/main/java/net/minestom/server/event/EventDispatcher.java index 742736e4e..6d0738ef6 100644 --- a/src/main/java/net/minestom/server/event/EventDispatcher.java +++ b/src/main/java/net/minestom/server/event/EventDispatcher.java @@ -10,10 +10,6 @@ public final class EventDispatcher { MinecraftServer.getGlobalEventHandler().call(event); } - public static void call(@NotNull E event, @NotNull ListenerHandle handle) { - MinecraftServer.getGlobalEventHandler().call(event, handle); - } - public static ListenerHandle getHandle(@NotNull Class handleType) { return MinecraftServer.getGlobalEventHandler().getHandle(handleType); } diff --git a/src/main/java/net/minestom/server/event/EventNode.java b/src/main/java/net/minestom/server/event/EventNode.java index a89d64713..621031355 100644 --- a/src/main/java/net/minestom/server/event/EventNode.java +++ b/src/main/java/net/minestom/server/event/EventNode.java @@ -188,22 +188,9 @@ public interface EventNode { */ default void call(@NotNull T event) { //noinspection unchecked - call(event, getHandle((Class) event.getClass())); + getHandle((Class) event.getClass()).call(event); } - /** - * Calls an event starting from this node. - *

- * The event handle can be retrieved using {@link #getHandle(Class)} - * and is useful to avoid map lookups for high-frequency events. - * - * @param event the event to call - * @param handle the event handle linked to this node - * @param the event type - * @throws IllegalArgumentException if {@code handle}'s owner is not {@code this} - */ - void call(@NotNull E event, @NotNull ListenerHandle handle); - /** * Gets the handle of an event type. * @@ -211,21 +198,9 @@ public interface EventNode { * @param the event type * @return the handle linked to {@code handleType} */ + @ApiStatus.Experimental @NotNull ListenerHandle getHandle(@NotNull Class handleType); - /** - * Gets if any listener has been registered for the given handle. - * May trigger an update if the cached data is not correct. - *

- * Useful if you are able to avoid expensive computation in the case where - * the event is unused. Be aware that {@link #call(Event, ListenerHandle)} - * has similar optimization built-in. - * - * @param handle the listener handle - * @return true if the event has 1 or more listeners - */ - boolean hasListener(@NotNull ListenerHandle handle); - /** * Execute a cancellable event with a callback to execute if the event is successful. * Event conditions and propagation is the same as {@link #call(Event)}. diff --git a/src/main/java/net/minestom/server/event/EventNodeImpl.java b/src/main/java/net/minestom/server/event/EventNodeImpl.java index e6179a0a7..495a6b1a1 100644 --- a/src/main/java/net/minestom/server/event/EventNodeImpl.java +++ b/src/main/java/net/minestom/server/event/EventNodeImpl.java @@ -39,18 +39,6 @@ class EventNodeImpl implements EventNode { this.eventType = filter.eventType(); } - @Override - public void call(@NotNull E event, @NotNull ListenerHandle handle) { - final Handle castedHandle = (Handle) handle; - Check.argCondition(castedHandle.node != this, "Invalid handle owner"); - if (!castedHandle.updated) castedHandle.update(); - final List> listeners = castedHandle.listeners; - if (listeners.isEmpty()) return; - for (Consumer listener : listeners) { - listener.accept(event); - } - } - @Override public @NotNull ListenerHandle getHandle(@NotNull Class handleType) { //noinspection unchecked @@ -58,13 +46,6 @@ class EventNodeImpl implements EventNode { aClass -> new Handle<>(this, (Class) aClass)); } - @Override - public boolean hasListener(@NotNull ListenerHandle handle) { - final Handle castedHandle = (Handle) handle; - if (!castedHandle.updated) castedHandle.update(); - return !castedHandle.listeners.isEmpty(); - } - @Override public @NotNull List> findChildren(@NotNull String name, Class eventType) { synchronized (GLOBAL_CHILD_LOCK) { @@ -239,6 +220,10 @@ class EventNodeImpl implements EventNode { for (Class eventType : listenerMap.keySet()) { node.invalidateEvent(eventType); } + // TODO bindings? + for (EventNodeImpl child : children) { + child.invalidateEventsFor(node); + } } private void invalidateEvent(Class eventClass) { @@ -277,7 +262,7 @@ class EventNodeImpl implements EventNode { private static final class Handle implements ListenerHandle { private final EventNodeImpl node; private final Class eventType; - private final List> listeners = new CopyOnWriteArrayList<>(); + private Consumer listener = null; private volatile boolean updated; Handle(EventNodeImpl node, Class eventType) { @@ -285,145 +270,170 @@ class EventNodeImpl implements EventNode { this.eventType = eventType; } - void update() { - synchronized (GLOBAL_CHILD_LOCK) { - this.listeners.clear(); - recursiveUpdate(node); - this.updated = true; + @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); } } - private void recursiveUpdate(EventNodeImpl targetNode) { + @Override + public boolean hasListener() { + return updatedListener() != null; + } + + @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() { // Standalone listeners + List> listeners = new ArrayList<>(); forTargetEvents(eventType, type -> { - final ListenerEntry entry = targetNode.listenerMap.get(type); - if (entry != null) appendEntries(entry, targetNode); + final ListenerEntry entry = node.listenerMap.get(type); + if (entry != null) { + final Consumer result = listenersConsumer(entry); + if (result != null) listeners.add(result); + } }); - // Mapped nodes - handleMappedNode(targetNode); - // Add children - final Set> children = targetNode.children; - if (children.isEmpty()) return; - children.stream() + 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)) - .forEach(this::recursiveUpdate); + .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 (listenersArray.length == 0 && mappedListener == null && childrenListeners.length == 0) { + // 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); + } + } + }; } /** - * Add the node's listeners from {@link EventNode#map(EventNode, Object)}. + * 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(EventNode, Object)}. * The goal is to limit the amount of map lookup. */ - private void handleMappedNode(EventNodeImpl targetNode) { - final var mappedNodeCache = targetNode.mappedNodeCache; - if (mappedNodeCache.isEmpty()) return; + private @Nullable Consumer mappedConsumer() { + final var mappedNodeCache = node.mappedNodeCache; + if (mappedNodeCache.isEmpty()) return null; Set> filters = new HashSet<>(mappedNodeCache.size()); Map> handlers = new HashMap<>(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 (!mappedNode.hasListener(handle)) continue; // Implicit update + 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()) { - final var filterList = List.copyOf(filters); - final int size = filterList.size(); - final BiConsumer, E> mapper = (filter, event) -> { - final Object handler = filter.castHandler(event); - final Handle handle = handlers.get(handler); - if (handle != null) { // Run the listeners of the mapped node - if (!handle.updated) handle.update(); - for (Consumer listener : handle.listeners) { - listener.accept(event); - } + 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); + }; + if (filterList.length == 1) { + final var firstFilter = filterList[0]; + // Common case where there is only one filter + return event -> mapper.accept(firstFilter, event); + } else if (filterList.length == 2) { + final var firstFilter = filterList[0]; + final var secondFilter = filterList[1]; + return event -> { + mapper.accept(firstFilter, event); + mapper.accept(secondFilter, event); + }; + } else { + return event -> { + for (var filter : filterList) { + mapper.accept(filter, event); } }; - if (size == 1) { - final var firstFilter = filterList.get(0); - // Common case where there is only one filter - this.listeners.add(event -> mapper.accept(firstFilter, event)); - } else if (size == 2) { - final var firstFilter = filterList.get(0); - final var secondFilter = filterList.get(1); - this.listeners.add(event -> { - mapper.accept(firstFilter, event); - mapper.accept(secondFilter, event); - }); - } else { - this.listeners.add(event -> { - for (var filter : filterList) { - mapper.accept(filter, event); - } - }); - } } } - /** - * Add listeners from {@link EventNode#addListener(EventListener)} and - * {@link EventNode#register(EventBinding)} to the handle list. - *

- * Most computation should ideally be done outside the consumers as a one-time cost. - */ - private void appendEntries(ListenerEntry entry, EventNodeImpl targetNode) { - final var filter = targetNode.filter; - final var predicate = targetNode.predicate; - - final boolean hasPredicate = predicate != null; - final List> listenersCopy = List.copyOf(entry.listeners); - final List> bindingsCopy = List.copyOf(entry.bindingConsumers); - if (!hasPredicate && listenersCopy.isEmpty() && bindingsCopy.isEmpty()) - return; // Nothing to run - - if (!hasPredicate && bindingsCopy.isEmpty() && listenersCopy.size() == 1) { - // Only one normal listener - final EventListener listener = listenersCopy.get(0); - this.listeners.add(e -> callListener(targetNode, listener, e)); - return; - } - - // Worse case scenario, try to run everything - this.listeners.add(e -> { - if (hasPredicate) { - final Object value = filter.getHandler(e); - try { - if (!predicate.test(e, value)) return; - } catch (Throwable t) { - MinecraftServer.getExceptionManager().handleException(t); - return; - } - } - if (!listenersCopy.isEmpty()) { - for (EventListener listener : listenersCopy) { - callListener(targetNode, listener, e); - } - } - if (!bindingsCopy.isEmpty()) { - for (Consumer eConsumer : bindingsCopy) { - try { - eConsumer.accept(e); - } catch (Throwable t) { - MinecraftServer.getExceptionManager().handleException(t); - } - } - } - }); - } - - static void callListener(EventNodeImpl targetNode, EventListener listener, E event) { - EventListener.Result result; - try { - result = listener.run(event); - } catch (Throwable t) { - result = EventListener.Result.EXCEPTION; - MinecraftServer.getExceptionManager().handleException(t); - } + void callListener(@NotNull EventListener listener, E event) { + EventListener.Result result = listener.run(event); if (result == EventListener.Result.EXPIRED) { - targetNode.removeListener(listener); + node.removeListener(listener); + this.updated = false; } } } diff --git a/src/main/java/net/minestom/server/event/ListenerHandle.java b/src/main/java/net/minestom/server/event/ListenerHandle.java index 8ddf7d3bf..5f3850695 100644 --- a/src/main/java/net/minestom/server/event/ListenerHandle.java +++ b/src/main/java/net/minestom/server/event/ListenerHandle.java @@ -1,13 +1,38 @@ package net.minestom.server.event; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** - * Represents a key to access an {@link EventNode} listeners. + * Represents a key to a listenable event, retrievable from {@link EventNode#getHandle(Class)}. * Useful to avoid map lookups. + *

+ * It is recommended to store instances of this class in {@code static final} fields. * * @param the event type */ +@ApiStatus.Experimental @ApiStatus.NonExtendable public interface ListenerHandle { + /** + * Calls the given event. + * Will try to fast exit the execution when possible if {@link #hasListener()} return {@code false}. + *

+ * Anonymous and subclasses are not supported, events must have the exact type {@code E}. + * + * @param event the event to call + */ + void call(@NotNull E event); + + /** + * Gets if any listener has been registered for the given handle. + * May trigger an update if the cached data is not correct. + *

+ * Useful if you are able to avoid expensive computation in the case where + * the event is unused. Be aware that {@link #call(Event)} + * has similar optimization built-in. + * + * @return true if the event has 1 or more listeners + */ + boolean hasListener(); } diff --git a/src/main/java/net/minestom/server/exception/ExceptionManager.java b/src/main/java/net/minestom/server/exception/ExceptionManager.java index e8964bf34..6abeb3500 100644 --- a/src/main/java/net/minestom/server/exception/ExceptionManager.java +++ b/src/main/java/net/minestom/server/exception/ExceptionManager.java @@ -15,6 +15,7 @@ public final class ExceptionManager { * @param e the occurred exception */ public void handleException(Throwable e) { + // TODO handle OOM exceptions this.getExceptionHandler().handleException(e); } diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index 7c486c0a7..0e4f0aa68 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -280,7 +280,7 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka if (result) { PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ); - EventDispatcher.call(playerChunkLoadEvent, GlobalHandles.PLAYER_CHUNK_LOAD); + GlobalHandles.PLAYER_CHUNK_LOAD.call(playerChunkLoadEvent); } return result; diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 46fd1adb2..c38eea11d 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -9,13 +9,13 @@ import net.minestom.server.entity.Player; import net.minestom.server.entity.pathfinding.PFBlock; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.network.packet.CachedPacket; import net.minestom.server.network.packet.FramedPacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.UpdateLightPacket; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; import net.minestom.server.utils.ArrayUtils; -import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.world.biomes.Biome; import org.jetbrains.annotations.NotNull; @@ -40,10 +40,8 @@ public class DynamicChunk extends Chunk { protected final Int2ObjectOpenHashMap tickableMap = new Int2ObjectOpenHashMap<>(); private volatile long lastChangeTime; - - private FramedPacket cachedChunkBuffer; - private FramedPacket cachedLightBuffer; - private long cachedPacketTime; + private final CachedPacket chunkCache = new CachedPacket(this::createChunkPacket); + private final CachedPacket lightCache = new CachedPacket(this::createLightPacket); public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ) { super(instance, biomes, chunkX, chunkZ, true); @@ -126,34 +124,31 @@ public class DynamicChunk extends Chunk { } @Override - public synchronized void sendChunk(@NotNull Player player) { + public void sendChunk(@NotNull Player player) { if (!isLoaded()) return; final PlayerConnection connection = player.getPlayerConnection(); + final long lastChange = getLastChangeTime(); + final FramedPacket lightPacket = lightCache.retrieveFramedPacket(lastChange); + final FramedPacket chunkPacket = chunkCache.retrieveFramedPacket(lastChange); if (connection instanceof PlayerSocketConnection) { - final long lastChange = getLastChangeTime(); - var chunkPacket = cachedChunkBuffer; - var lightPacket = cachedLightBuffer; - if (lastChange > cachedPacketTime || (chunkPacket == null || lightPacket == null)) { - chunkPacket = PacketUtils.allocateTrimmedPacket(createChunkPacket()); - lightPacket = PacketUtils.allocateTrimmedPacket(createLightPacket()); - this.cachedChunkBuffer = chunkPacket; - this.cachedLightBuffer = lightPacket; - this.cachedPacketTime = lastChange; - } PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection; - socketConnection.write(lightPacket); - socketConnection.write(chunkPacket); + socketConnection.write(lightPacket.body()); + socketConnection.write(chunkPacket.body()); } else { - connection.sendPacket(createLightPacket()); - connection.sendPacket(createChunkPacket()); + connection.sendPacket(lightPacket.packet()); + connection.sendPacket(chunkPacket.packet()); } } @Override - public synchronized void sendChunk() { + public void sendChunk() { if (!isLoaded()) return; - sendPacketToViewers(createLightPacket()); - sendPacketToViewers(createChunkPacket()); + if (getViewers().isEmpty()) return; + final long lastChange = getLastChangeTime(); + final FramedPacket lightPacket = lightCache.retrieveFramedPacket(lastChange); + final FramedPacket chunkPacket = chunkCache.retrieveFramedPacket(lastChange); + sendPacketToViewers(lightPacket.packet()); + sendPacketToViewers(chunkPacket.packet()); } @NotNull diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index 222cee95e..4dc1b8623 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -139,8 +139,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta } @ApiStatus.Internal - public abstract boolean placeBlock(@NotNull Player player, @NotNull Block block, @NotNull Point blockPosition, - @NotNull BlockFace blockFace, float cursorX, float cursorY, float cursorZ); + public abstract boolean placeBlock(@NotNull BlockHandler.Placement placement); /** * Does call {@link net.minestom.server.event.player.PlayerBlockBreakEvent} @@ -732,7 +731,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta // Tick event { // Process tick events - EventDispatcher.call(new InstanceTickEvent(this, time, lastTickAge), GlobalHandles.INSTANCE_TICK); + GlobalHandles.INSTANCE_TICK.call(new InstanceTickEvent(this, time, lastTickAge)); // Set last tick age this.lastTickAge = time; } diff --git a/src/main/java/net/minestom/server/instance/InstanceContainer.java b/src/main/java/net/minestom/server/instance/InstanceContainer.java index ed177b272..3a320cf6d 100644 --- a/src/main/java/net/minestom/server/instance/InstanceContainer.java +++ b/src/main/java/net/minestom/server/instance/InstanceContainer.java @@ -13,7 +13,6 @@ import net.minestom.server.event.instance.InstanceChunkUnloadEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.instance.batch.ChunkGenerationBatch; import net.minestom.server.instance.block.Block; -import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.instance.block.rule.BlockPlacementRule; import net.minestom.server.network.packet.server.play.BlockChangePacket; @@ -27,6 +26,7 @@ import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.DimensionType; import net.minestom.server.world.biomes.Biome; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -68,21 +68,18 @@ public class InstanceContainer extends Instance { protected InstanceContainer srcInstance; // only present if this instance has been created using a copy private long lastBlockChangeTime; // Time at which the last block change happened (#setBlock) - /** - * Creates an {@link InstanceContainer}. - * - * @param uniqueId the unique id of the instance - * @param dimensionType the dimension type of the instance - */ - public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) { + @ApiStatus.Experimental + public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) { super(uniqueId, dimensionType); - // Set the default chunk supplier using DynamicChunk setChunkSupplier(DynamicChunk::new); - // Set the default chunk loader which use the Anvil format - setChunkLoader(new AnvilLoader("world")); + setChunkLoader(Objects.requireNonNullElseGet(loader, () -> new AnvilLoader("world"))); this.chunkLoader.loadInstance(this); } + public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) { + this(uniqueId, dimensionType, null); + } + @Override public void setBlock(int x, int y, int z, @NotNull Block block) { final Chunk chunk = getChunkAt(x, z); @@ -156,12 +153,12 @@ public class InstanceContainer extends Instance { } @Override - public boolean placeBlock(@NotNull Player player, @NotNull Block block, @NotNull Point blockPosition, - @NotNull BlockFace blockFace, float cursorX, float cursorY, float cursorZ) { + public boolean placeBlock(@NotNull BlockHandler.Placement placement) { + final Point blockPosition = placement.getBlockPosition(); final Chunk chunk = getChunkAt(blockPosition); if (!ChunkUtils.isLoaded(chunk)) return false; - UNSAFE_setBlock(chunk, blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(), block, - new BlockHandler.PlayerPlacement(block, this, blockPosition, player, blockFace, cursorX, cursorY, cursorZ), null); + UNSAFE_setBlock(chunk, blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(), + placement.getBlock(), placement, null); return true; } @@ -275,7 +272,7 @@ public class InstanceContainer extends Instance { .whenComplete((chunk, throwable) -> { // TODO run in the instance thread? cacheChunk(chunk); - EventDispatcher.call(new InstanceChunkLoadEvent(this, chunkX, chunkZ), GlobalHandles.INSTANCE_CHUNK_LOAD); + GlobalHandles.INSTANCE_CHUNK_LOAD.call(new InstanceChunkLoadEvent(this, chunkX, chunkZ)); synchronized (loadingChunks) { this.loadingChunks.remove(ChunkUtils.getChunkIndex(chunk)); } diff --git a/src/main/java/net/minestom/server/instance/InstanceManager.java b/src/main/java/net/minestom/server/instance/InstanceManager.java index 0dfcd9016..4dbece1a2 100644 --- a/src/main/java/net/minestom/server/instance/InstanceManager.java +++ b/src/main/java/net/minestom/server/instance/InstanceManager.java @@ -4,6 +4,7 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.storage.StorageLocation; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.DimensionType; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,21 +40,32 @@ public final class InstanceManager { * with the specified {@link DimensionType} and {@link StorageLocation}. * * @param dimensionType the {@link DimensionType} of the instance + * @param loader the chunk loader * @return the created {@link InstanceContainer} */ - public @NotNull InstanceContainer createInstanceContainer(@NotNull DimensionType dimensionType) { - final InstanceContainer instanceContainer = new InstanceContainer(UUID.randomUUID(), dimensionType); + @ApiStatus.Experimental + public @NotNull InstanceContainer createInstanceContainer(@NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) { + final InstanceContainer instanceContainer = new InstanceContainer(UUID.randomUUID(), dimensionType, loader); registerInstance(instanceContainer); return instanceContainer; } + public @NotNull InstanceContainer createInstanceContainer(@NotNull DimensionType dimensionType) { + return createInstanceContainer(dimensionType, null); + } + + @ApiStatus.Experimental + public @NotNull InstanceContainer createInstanceContainer(@Nullable IChunkLoader loader) { + return createInstanceContainer(DimensionType.OVERWORLD, loader); + } + /** * Creates and register an {@link InstanceContainer}. * * @return the created {@link InstanceContainer} */ public @NotNull InstanceContainer createInstanceContainer() { - return createInstanceContainer(DimensionType.OVERWORLD); + return createInstanceContainer(DimensionType.OVERWORLD, null); } /** diff --git a/src/main/java/net/minestom/server/instance/SharedInstance.java b/src/main/java/net/minestom/server/instance/SharedInstance.java index df2b80391..a4ae6eaaf 100644 --- a/src/main/java/net/minestom/server/instance/SharedInstance.java +++ b/src/main/java/net/minestom/server/instance/SharedInstance.java @@ -3,7 +3,7 @@ package net.minestom.server.instance; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.block.Block; -import net.minestom.server.instance.block.BlockFace; +import net.minestom.server.instance.block.BlockHandler; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -28,9 +28,8 @@ public class SharedInstance extends Instance { } @Override - public boolean placeBlock(@NotNull Player player, @NotNull Block block, @NotNull Point blockPosition, - @NotNull BlockFace blockFace, float cursorX, float cursorY, float cursorZ) { - return instanceContainer.placeBlock(player, block, blockPosition, blockFace, cursorX, cursorY, cursorZ); + public boolean placeBlock(@NotNull BlockHandler.Placement placement) { + return instanceContainer.placeBlock(placement); } @Override diff --git a/src/main/java/net/minestom/server/instance/block/BlockHandler.java b/src/main/java/net/minestom/server/instance/block/BlockHandler.java index b07c5f8f4..acd794dc1 100644 --- a/src/main/java/net/minestom/server/instance/block/BlockHandler.java +++ b/src/main/java/net/minestom/server/instance/block/BlockHandler.java @@ -114,14 +114,16 @@ public interface BlockHandler { final class PlayerPlacement extends Placement { private final Player player; + private final Player.Hand hand; private final BlockFace blockFace; private final float cursorX, cursorY, cursorZ; @ApiStatus.Internal public PlayerPlacement(Block block, Instance instance, Point blockPosition, - Player player, BlockFace blockFace, float cursorX, float cursorY, float cursorZ) { + Player player, Player.Hand hand, BlockFace blockFace, float cursorX, float cursorY, float cursorZ) { super(block, instance, blockPosition); this.player = player; + this.hand = hand; this.blockFace = blockFace; this.cursorX = cursorX; this.cursorY = cursorY; @@ -132,6 +134,10 @@ public interface BlockHandler { return player; } + public @NotNull Player.Hand getHand() { + return hand; + } + public @NotNull BlockFace getBlockFace() { return blockFace; } 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 df311d2f4..a39048b62 100644 --- a/src/main/java/net/minestom/server/instance/block/BlockImpl.java +++ b/src/main/java/net/minestom/server/instance/block/BlockImpl.java @@ -173,7 +173,7 @@ final class BlockImpl implements Block { private Block compute(Map properties) { Block block = propertyEntry.get(properties); if (block == null) - throw new IllegalArgumentException("Invalid properties: " + properties); + throw new IllegalArgumentException("Invalid properties: " + properties + " for block " + this); return nbt == null && handler == null ? block : new BlockImpl(block.registry(), propertyEntry, block.properties(), nbt, handler); } diff --git a/src/main/java/net/minestom/server/inventory/Inventory.java b/src/main/java/net/minestom/server/inventory/Inventory.java index edc155626..c8a253939 100644 --- a/src/main/java/net/minestom/server/inventory/Inventory.java +++ b/src/main/java/net/minestom/server/inventory/Inventory.java @@ -401,18 +401,7 @@ public class Inventory extends AbstractInventory implements Viewable { final InventoryClickResult clickResult = clickProcessor.dragging(player, slot != -999 ? (isInWindow ? this : playerInventory) : null, clickSlot, button, - clicked, cursor, - - s -> isClickInWindow(s) ? getItemStack(s) : - playerInventory.getItemStack(PlayerInventoryUtils.convertSlot(s, offset)), - - (s, item) -> { - if (isClickInWindow(s)) { - setItemStack(s, item); - } else { - playerInventory.setItemStack(PlayerInventoryUtils.convertSlot(s, offset), item); - } - }); + clicked, cursor); if (clickResult == null || clickResult.isCancel()) { updateAll(player); return false; diff --git a/src/main/java/net/minestom/server/inventory/PlayerInventory.java b/src/main/java/net/minestom/server/inventory/PlayerInventory.java index af3fbe2d2..def1fdca1 100644 --- a/src/main/java/net/minestom/server/inventory/PlayerInventory.java +++ b/src/main/java/net/minestom/server/inventory/PlayerInventory.java @@ -317,9 +317,7 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl final ItemStack cursor = getCursorItem(); final ItemStack clicked = slot != -999 ? getItemStackFromPacketSlot(slot) : ItemStack.AIR; final InventoryClickResult clickResult = clickProcessor.dragging(player, this, - slot, button, - clicked, cursor, this::getItemStackFromPacketSlot, - this::setItemStackFromPacketSlot); + convertPlayerInventorySlot(slot, OFFSET), button, clicked, cursor); if (clickResult == null || clickResult.isCancel()) { update(); return false; diff --git a/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java b/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java index acc8ffa10..b88ba53b7 100644 --- a/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java +++ b/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java @@ -1,8 +1,5 @@ package net.minestom.server.inventory.click; -import it.unimi.dsi.fastutil.ints.Int2ObjectFunction; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; @@ -18,18 +15,18 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; import java.util.function.BiFunction; @ApiStatus.Internal public final class InventoryClickProcessor { // Dragging maps - private final Map leftDraggingMap = new ConcurrentHashMap<>(); - private final Map rightDraggingMap = new ConcurrentHashMap<>(); + private final Map> leftDraggingMap = new ConcurrentHashMap<>(); + private final Map> rightDraggingMap = new ConcurrentHashMap<>(); public @NotNull InventoryClickResult leftClick(@NotNull Player player, @NotNull AbstractInventory inventory, int slot, @@ -162,23 +159,21 @@ public final class InventoryClickProcessor { public @Nullable InventoryClickResult dragging(@NotNull Player player, @Nullable AbstractInventory inventory, int slot, int button, - @NotNull ItemStack clicked, @NotNull ItemStack cursor, - @NotNull Int2ObjectFunction itemGetter, - @NotNull BiConsumer itemSetter) { + @NotNull ItemStack clicked, @NotNull ItemStack cursor) { InventoryClickResult clickResult = null; final StackingRule stackingRule = cursor.getStackingRule(); if (slot != -999) { // Add slot if (button == 1) { // Add left - IntSet left = leftDraggingMap.get(player); + List left = leftDraggingMap.get(player); if (left == null) return null; - left.add(slot); + left.add(new DragData(slot, inventory)); } else if (button == 5) { // Add right - IntSet right = rightDraggingMap.get(player); + List right = rightDraggingMap.get(player); if (right == null) return null; - right.add(slot); + right.add(new DragData(slot, inventory)); } else if (button == 9) { // Add middle // TODO @@ -188,18 +183,18 @@ public final class InventoryClickProcessor { if (button == 0) { // Start left clickResult = startCondition(player, inventory, slot, ClickType.START_LEFT_DRAGGING, clicked, cursor); - if (!clickResult.isCancel()) this.leftDraggingMap.put(player, new IntOpenHashSet()); + if (!clickResult.isCancel()) this.leftDraggingMap.put(player, new ArrayList<>()); } else if (button == 2) { // End left - final IntSet slots = leftDraggingMap.remove(player); + final List slots = leftDraggingMap.remove(player); if (slots == null) return null; final int slotCount = slots.size(); final int cursorAmount = stackingRule.getAmount(cursor); if (slotCount > cursorAmount) return null; - for (int s : slots) { + for (DragData s : slots) { // Apply each drag element - final ItemStack slotItem = itemGetter.apply(s); - clickResult = startCondition(player, inventory, s, ClickType.LEFT_DRAGGING, slotItem, cursor); + final ItemStack slotItem = s.inventory.getItemStack(s.slot); + clickResult = startCondition(player, s.inventory, s.slot, ClickType.LEFT_DRAGGING, slotItem, cursor); if (clickResult.isCancel()) { return clickResult; } @@ -210,8 +205,10 @@ public final class InventoryClickProcessor { final int slotSize = (int) ((float) cursorAmount / (float) slotCount); // Place all waiting drag action int finalCursorAmount = cursorAmount; - for (int s : slots) { - ItemStack slotItem = itemGetter.apply(s); + for (DragData dragData : slots) { + final var inv = dragData.inventory; + final int s = dragData.slot; + ItemStack slotItem = inv.getItemStack(s); final StackingRule slotItemRule = slotItem.getStackingRule(); final int amount = slotItemRule.getAmount(slotItem); if (stackingRule.canBeStacked(cursor, slotItem)) { @@ -231,7 +228,7 @@ public final class InventoryClickProcessor { slotItem = stackingRule.apply(cursor, slotSize); finalCursorAmount -= slotSize; } - itemSetter.accept(s, slotItem); + inv.setItemStack(s, slotItem); callClickEvent(player, inventory, s, ClickType.LEFT_DRAGGING, slotItem, cursor); } // Update the cursor @@ -239,18 +236,18 @@ public final class InventoryClickProcessor { } else if (button == 4) { // Start right clickResult = startCondition(player, inventory, slot, ClickType.START_RIGHT_DRAGGING, clicked, cursor); - if (!clickResult.isCancel()) this.rightDraggingMap.put(player, new IntOpenHashSet()); + if (!clickResult.isCancel()) this.rightDraggingMap.put(player, new ArrayList<>()); } else if (button == 6) { // End right - final IntSet slots = rightDraggingMap.remove(player); + final List slots = rightDraggingMap.remove(player); if (slots == null) return null; final int size = slots.size(); int cursorAmount = stackingRule.getAmount(cursor); if (size > cursorAmount) return null; // Verify if each slot can be modified (or cancel the whole drag) - for (int s : slots) { - ItemStack slotItem = itemGetter.apply(s); - clickResult = startCondition(player, inventory, s, ClickType.RIGHT_DRAGGING, slotItem, cursor); + for (DragData s : slots) { + final ItemStack slotItem = s.inventory.getItemStack(s.slot); + clickResult = startCondition(player, s.inventory, s.slot, ClickType.RIGHT_DRAGGING, slotItem, cursor); if (clickResult.isCancel()) { return clickResult; } @@ -259,8 +256,10 @@ public final class InventoryClickProcessor { if (clickResult.isCancel()) return clickResult; // Place all waiting drag action int finalCursorAmount = cursorAmount; - for (int s : slots) { - ItemStack slotItem = itemGetter.apply(s); + for (DragData dragData : slots) { + final var inv = dragData.inventory; + final int s = dragData.slot; + ItemStack slotItem = inv.getItemStack(s); StackingRule slotItemRule = slotItem.getStackingRule(); if (stackingRule.canBeStacked(cursor, slotItem)) { // Compatible item in the slot, increment by 1 @@ -274,7 +273,7 @@ public final class InventoryClickProcessor { slotItem = stackingRule.apply(cursor, 1); finalCursorAmount -= 1; } - itemSetter.accept(s, slotItem); + inv.setItemStack(s, slotItem); callClickEvent(player, inventory, s, ClickType.RIGHT_DRAGGING, slotItem, cursor); } // Update the cursor @@ -462,4 +461,14 @@ public final class InventoryClickProcessor { this.leftDraggingMap.remove(player); this.rightDraggingMap.remove(player); } + + private static final class DragData { + private final int slot; + private final AbstractInventory inventory; + + public DragData(int slot, AbstractInventory inventory) { + this.slot = slot; + this.inventory = inventory; + } + } } diff --git a/src/main/java/net/minestom/server/item/ItemStack.java b/src/main/java/net/minestom/server/item/ItemStack.java index 9d42260dc..8c8b55066 100644 --- a/src/main/java/net/minestom/server/item/ItemStack.java +++ b/src/main/java/net/minestom/server/item/ItemStack.java @@ -7,6 +7,7 @@ import net.minestom.server.item.rule.VanillaStackingRule; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagReadable; import net.minestom.server.utils.NBTUtils; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -79,6 +80,24 @@ public final class ItemStack implements TagReadable, HoverEventSource { + + private final List items; + + protected BundleMeta(ItemMetaBuilder metaBuilder, + @NotNull List items) { + super(metaBuilder); + this.items = List.copyOf(items); + } + + public @NotNull List getItems() { + return items; + } + + public static class Builder extends ItemMetaBuilder { + private List items = new ArrayList<>(); + + public Builder items(@NotNull List items) { + this.items = new ArrayList<>(items); // defensive copy + updateItems(); + return this; + } + + @ApiStatus.Experimental + public Builder addItem(@NotNull ItemStack item) { + items.add(item); + updateItems(); + return this; + } + + @ApiStatus.Experimental + public Builder removeItem(@NotNull ItemStack item) { + items.remove(item); + updateItems(); + return this; + } + + @Override + public @NotNull BundleMeta build() { + return new BundleMeta(this, items); + } + + private void updateItems() { + mutateNbt(compound -> { + NBTList itemList = new NBTList<>(NBTTypes.TAG_Compound); + for (ItemStack item : items) { + itemList.add(item.toItemNBT()); + } + compound.set("Items", itemList); + }); + } + + @Override + public void read(@NotNull NBTCompound nbtCompound) { + if (nbtCompound.containsKey("Items")) { + final NBTList items = nbtCompound.getList("Items"); + for (NBTCompound item : items) { + this.items.add(ItemStack.fromItemNBT(item)); + } + } + } + + @Override + protected @NotNull Supplier<@NotNull ItemMetaBuilder> getSupplier() { + return Builder::new; + } + } +} diff --git a/src/main/java/net/minestom/server/listener/BlockPlacementListener.java b/src/main/java/net/minestom/server/listener/BlockPlacementListener.java index 92ca45854..1cc3a06a8 100644 --- a/src/main/java/net/minestom/server/listener/BlockPlacementListener.java +++ b/src/main/java/net/minestom/server/listener/BlockPlacementListener.java @@ -157,8 +157,8 @@ public class BlockPlacementListener { return; } // Place the block - instance.placeBlock(player, resultBlock, placementPosition, - blockFace, packet.cursorPositionX, packet.cursorPositionY, packet.cursorPositionZ); + instance.placeBlock(new BlockHandler.PlayerPlacement(resultBlock, instance, placementPosition, player, hand, blockFace, + packet.cursorPositionX, packet.cursorPositionY, packet.cursorPositionZ)); // Block consuming if (playerBlockPlaceEvent.doesConsumeBlock()) { // Consume the block in the player's hand diff --git a/src/main/java/net/minestom/server/listener/PlayerPositionListener.java b/src/main/java/net/minestom/server/listener/PlayerPositionListener.java index 9ca1e1f3d..da11e1a13 100644 --- a/src/main/java/net/minestom/server/listener/PlayerPositionListener.java +++ b/src/main/java/net/minestom/server/listener/PlayerPositionListener.java @@ -2,11 +2,12 @@ package net.minestom.server.listener; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalHandles; import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.instance.Instance; import net.minestom.server.network.packet.client.play.*; +import net.minestom.server.network.packet.server.play.PlayerPositionAndLookPacket; +import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.utils.chunk.ChunkUtils; import org.jetbrains.annotations.NotNull; @@ -32,9 +33,9 @@ public class PlayerPositionListener { player.refreshReceivedTeleportId(packet.teleportId); } - private static void processMovement(@NotNull Player player, @NotNull Pos newPosition, boolean onGround) { + private static void processMovement(@NotNull Player player, @NotNull Pos packetPosition, boolean onGround) { final var currentPosition = player.getPosition(); - if (currentPosition.equals(newPosition)) { + if (currentPosition.equals(packetPosition)) { // For some reason, the position is the same return; } @@ -48,21 +49,37 @@ public class PlayerPositionListener { return; } // Try to move in an unloaded chunk, prevent it - if (!currentPosition.sameChunk(newPosition) && !ChunkUtils.isLoaded(instance, newPosition)) { + if (!currentPosition.sameChunk(packetPosition) && !ChunkUtils.isLoaded(instance, packetPosition)) { player.teleport(currentPosition); return; } - PlayerMoveEvent playerMoveEvent = new PlayerMoveEvent(player, newPosition); - EventDispatcher.call(playerMoveEvent, GlobalHandles.PLAYER_MOVE); - // True if the event call changed the player position (possibly a teleport) - if (!playerMoveEvent.isCancelled() && currentPosition.equals(player.getPosition())) { - // Move the player - player.refreshPosition(playerMoveEvent.getNewPosition()); + PlayerMoveEvent playerMoveEvent = new PlayerMoveEvent(player, packetPosition); + GlobalHandles.PLAYER_MOVE.call(playerMoveEvent); + if (!currentPosition.equals(player.getPosition())) { + // Player has been teleported in the event + return; + } + if (playerMoveEvent.isCancelled()) { + // Teleport to previous position + PlayerConnection connection = player.getPlayerConnection(); + connection.sendPacket(new PlayerPositionAndLookPacket(currentPosition, (byte) 0x00, player.getNextTeleportId(), false)); + return; + } + final Pos eventPosition = playerMoveEvent.getNewPosition(); + if (packetPosition.equals(eventPosition)) { + // Event didn't change the position + player.refreshPosition(eventPosition); player.refreshOnGround(onGround); } else { - // Cancelled, teleport to previous position - player.teleport(player.getPosition()); + // Position modified by the event + if (packetPosition.samePoint(eventPosition)) { + player.refreshPosition(eventPosition, true); + player.refreshOnGround(onGround); + player.setView(eventPosition.yaw(), eventPosition.pitch()); + } else { + player.teleport(eventPosition); + } } } } diff --git a/src/main/java/net/minestom/server/listener/WindowListener.java b/src/main/java/net/minestom/server/listener/WindowListener.java index b84420504..2ac80cd70 100644 --- a/src/main/java/net/minestom/server/listener/WindowListener.java +++ b/src/main/java/net/minestom/server/listener/WindowListener.java @@ -66,8 +66,6 @@ public class WindowListener { successful = inventory.doubleClick(player, slot); } - // Prevent the player from picking a ghost item in cursor - refreshCursorItem(player, inventory); // Prevent ghost item when the click is cancelled if (!successful) { player.getInventory().update(); @@ -75,6 +73,10 @@ public class WindowListener { ((Inventory) inventory).update(player); } } + + // Prevent the player from picking a ghost item in cursor + refreshCursorItem(player, inventory); + // (Why is the ping packet necessary?) PingPacket pingPacket = new PingPacket(); pingPacket.id = (1 << 30) | (windowId << 16); diff --git a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java index e75d83e5a..b0ec7d267 100644 --- a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java +++ b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java @@ -2,7 +2,6 @@ package net.minestom.server.listener.manager; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalHandles; import net.minestom.server.event.player.PlayerPacketEvent; import net.minestom.server.listener.*; @@ -90,7 +89,7 @@ public final class PacketListenerManager { // Event PlayerPacketEvent playerPacketEvent = new PlayerPacketEvent(player, packet); - EventDispatcher.call(playerPacketEvent, GlobalHandles.PLAYER_PACKET); + GlobalHandles.PLAYER_PACKET.call(playerPacketEvent); if (playerPacketEvent.isCancelled()) { return; } diff --git a/src/main/java/net/minestom/server/network/packet/CachedPacket.java b/src/main/java/net/minestom/server/network/packet/CachedPacket.java new file mode 100644 index 000000000..d06654424 --- /dev/null +++ b/src/main/java/net/minestom/server/network/packet/CachedPacket.java @@ -0,0 +1,31 @@ +package net.minestom.server.network.packet; + +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.utils.PacketUtils; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.ref.SoftReference; +import java.util.function.Supplier; + +@ApiStatus.Internal +public final class CachedPacket { + private final Supplier supplier; + private volatile long packetTimestamp; + private SoftReference packet; + + public CachedPacket(Supplier supplier) { + this.supplier = supplier; + } + + public FramedPacket retrieveFramedPacket(long lastChange) { + final long timestamp = packetTimestamp; + final var ref = packet; + FramedPacket cache = ref != null ? ref.get() : null; + if (cache == null || lastChange > timestamp) { + cache = PacketUtils.allocateTrimmedPacket(supplier.get()); + this.packet = new SoftReference<>(cache); + this.packetTimestamp = lastChange; + } + return cache; + } +} diff --git a/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java b/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java index c8ae92419..d418eba3d 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java @@ -12,7 +12,6 @@ import net.minestom.server.network.packet.FramedPacket; import net.minestom.server.network.packet.server.ComponentHoldingServerPacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.login.SetCompressionPacket; -import net.minestom.server.network.socket.Server; import net.minestom.server.network.socket.Worker; import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.Utils; @@ -32,10 +31,9 @@ import java.net.SocketAddress; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.zip.DataFormatException; /** @@ -46,6 +44,8 @@ import java.util.zip.DataFormatException; @ApiStatus.Internal public class PlayerSocketConnection extends PlayerConnection { private final static Logger LOGGER = LoggerFactory.getLogger(PlayerSocketConnection.class); + private final static Queue POOLED_BUFFERS = new ConcurrentLinkedQueue<>(); + private final static int BUFFER_SIZE = 262_143; private final Worker worker; private final SocketChannel channel; @@ -73,7 +73,9 @@ public class PlayerSocketConnection extends PlayerConnection { private UUID bungeeUuid; private PlayerSkin bungeeSkin; - private final BinaryBuffer tickBuffer = BinaryBuffer.ofSize(Server.SOCKET_BUFFER_SIZE); + private final Object bufferLock = new Object(); + private final List waitingBuffers = new ArrayList<>(); + private BinaryBuffer tickBuffer = BinaryBuffer.ofSize(BUFFER_SIZE); private volatile BinaryBuffer cacheBuffer; public PlayerSocketConnection(@NotNull Worker worker, @NotNull SocketChannel channel, SocketAddress remoteAddress) { @@ -227,7 +229,7 @@ public class PlayerSocketConnection extends PlayerConnection { } public void write(@NotNull ByteBuffer buffer) { - synchronized (tickBuffer) { + synchronized (bufferLock) { if (!tickBuffer.canWrite(buffer.position())) { // Tick buffer is full, flush before appending flush(); @@ -246,7 +248,7 @@ public class PlayerSocketConnection extends PlayerConnection { } public void writeAndFlush(@NotNull ServerPacket packet) { - synchronized (tickBuffer) { + synchronized (bufferLock) { write(packet); flush(); } @@ -255,31 +257,45 @@ public class PlayerSocketConnection extends PlayerConnection { @Override public void flush() { if (!channel.isOpen()) return; - synchronized (tickBuffer) { - if (tickBuffer.readableBytes() == 0) return; - try { - if (encrypted) { - final Cipher cipher = encryptCipher; - // Encrypt data first - final int remainingBytes = tickBuffer.readableBytes(); - final byte[] bytes = tickBuffer.readRemainingBytes(); - byte[] outTempArray = new byte[cipher.getOutputSize(remainingBytes)]; + if (tickBuffer.readableBytes() == 0 && waitingBuffers.isEmpty()) return; + synchronized (bufferLock) { + if (tickBuffer.readableBytes() == 0 && waitingBuffers.isEmpty()) return; + if (encrypted) { + final Cipher cipher = encryptCipher; + // Encrypt data first + final int remainingBytes = tickBuffer.readableBytes(); + final byte[] bytes = tickBuffer.readRemainingBytes(); + byte[] outTempArray = new byte[cipher.getOutputSize(remainingBytes)]; + try { cipher.update(bytes, 0, remainingBytes, outTempArray); - this.tickBuffer.clear(); - this.tickBuffer.writeBytes(outTempArray); - } - this.tickBuffer.writeChannel(channel); - } catch (IOException e) { - final String message = e.getMessage(); - if (message == null || - (!message.equals("Broken pipe") && !message.equals("Connection reset by peer"))) { + } catch (ShortBufferException e) { MinecraftServer.getExceptionManager().handleException(e); } - } catch (ShortBufferException e) { - e.printStackTrace(); - } finally { this.tickBuffer.clear(); + this.tickBuffer.writeBytes(outTempArray); } + + this.waitingBuffers.add(tickBuffer); + Iterator iterator = waitingBuffers.iterator(); + while (iterator.hasNext()) { + BinaryBuffer waitingBuffer = iterator.next(); + try { + if (!waitingBuffer.writeChannel(channel)) break; + iterator.remove(); + waitingBuffer.clear(); + POOLED_BUFFERS.add(waitingBuffer); + } catch (IOException e) { + final String message = e.getMessage(); + if (message == null || + (!message.equals("Broken pipe") && !message.equals("Connection reset by peer"))) { + MinecraftServer.getExceptionManager().handleException(e); + } } + } + // Update tick buffer + BinaryBuffer newBuffer = POOLED_BUFFERS.poll(); + if (newBuffer == null) newBuffer = BinaryBuffer.ofSize(BUFFER_SIZE); + newBuffer.clear(); + this.tickBuffer = newBuffer; } } @@ -303,6 +319,9 @@ public class PlayerSocketConnection extends PlayerConnection { @Override public void disconnect() { this.worker.disconnect(this, channel); + synchronized (bufferLock) { + POOLED_BUFFERS.addAll(waitingBuffers); + } } public @NotNull SocketChannel getChannel() { diff --git a/src/main/java/net/minestom/server/network/socket/Server.java b/src/main/java/net/minestom/server/network/socket/Server.java index 36ad7a262..2725fac9f 100644 --- a/src/main/java/net/minestom/server/network/socket/Server.java +++ b/src/main/java/net/minestom/server/network/socket/Server.java @@ -18,8 +18,8 @@ public final class Server { public static final Logger LOGGER = LoggerFactory.getLogger(Server.class); public static final int WORKER_COUNT = Integer.getInteger("minestom.workers", Runtime.getRuntime().availableProcessors()); - public static final int SOCKET_BUFFER_SIZE = Integer.getInteger("minestom.buffer-size", 1_048_575); public static final int MAX_PACKET_SIZE = 2_097_151; // 3 bytes var-int + public static final int SOCKET_BUFFER_SIZE = Integer.getInteger("minestom.buffer-size", MAX_PACKET_SIZE); public static final boolean NO_DELAY = true; private volatile boolean stop; diff --git a/src/main/java/net/minestom/server/network/socket/Worker.java b/src/main/java/net/minestom/server/network/socket/Worker.java index 54b94d61d..eb23c87f8 100644 --- a/src/main/java/net/minestom/server/network/socket/Worker.java +++ b/src/main/java/net/minestom/server/network/socket/Worker.java @@ -84,6 +84,7 @@ public final class Worker extends Thread { socket.setSendBufferSize(Server.SOCKET_BUFFER_SIZE); socket.setReceiveBufferSize(Server.SOCKET_BUFFER_SIZE); socket.setTcpNoDelay(Server.NO_DELAY); + socket.setSoTimeout(30 * 1000); // 30 seconds this.selector.wakeup(); } diff --git a/src/main/java/net/minestom/server/thread/ThreadProvider.java b/src/main/java/net/minestom/server/thread/ThreadProvider.java index 467d2be36..084b0c9a7 100644 --- a/src/main/java/net/minestom/server/thread/ThreadProvider.java +++ b/src/main/java/net/minestom/server/thread/ThreadProvider.java @@ -135,7 +135,7 @@ public abstract class ThreadProvider { return; try { chunk.tick(time); - } catch (Exception e) { + } catch (Throwable e) { MinecraftServer.getExceptionManager().handleException(e); } final var entities = chunkEntry.entities; @@ -148,7 +148,7 @@ public abstract class ThreadProvider { } try { entity.tick(time); - } catch (Exception e) { + } catch (Throwable e) { MinecraftServer.getExceptionManager().handleException(e); } } diff --git a/src/main/java/net/minestom/server/timer/Task.java b/src/main/java/net/minestom/server/timer/Task.java index a97d1ca1c..8b7e8218a 100644 --- a/src/main/java/net/minestom/server/timer/Task.java +++ b/src/main/java/net/minestom/server/timer/Task.java @@ -100,12 +100,26 @@ public class Task implements Runnable { * Sets up the task for correct execution. */ public void schedule() { - if(owningExtension != null) { - this.schedulerManager.onScheduleFromExtension(owningExtension, this); + if(this.shutdown) { + Int2ObjectMap shutdownTasks = this.schedulerManager.shutdownTasks; + synchronized (shutdownTasks) { + shutdownTasks.put(getId(), this); + } + if (owningExtension != null) { + this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, this); + } + } else { + Int2ObjectMap tasks = this.schedulerManager.tasks; + synchronized (tasks) { + tasks.put(getId(), this); + } + if (owningExtension != null) { + this.schedulerManager.onScheduleFromExtension(owningExtension, this); + } + this.future = this.repeat == 0L ? + this.schedulerManager.getTimerExecutionService().schedule(this, this.delay, TimeUnit.MILLISECONDS) : + this.schedulerManager.getTimerExecutionService().scheduleAtFixedRate(this, this.delay, this.repeat, TimeUnit.MILLISECONDS); } - this.future = this.repeat == 0L ? - this.schedulerManager.getTimerExecutionService().schedule(this, this.delay, TimeUnit.MILLISECONDS) : - this.schedulerManager.getTimerExecutionService().scheduleAtFixedRate(this, this.delay, this.repeat, TimeUnit.MILLISECONDS); } /** diff --git a/src/main/java/net/minestom/server/timer/TaskBuilder.java b/src/main/java/net/minestom/server/timer/TaskBuilder.java index d8cf7e6d5..f1387f0c7 100644 --- a/src/main/java/net/minestom/server/timer/TaskBuilder.java +++ b/src/main/java/net/minestom/server/timer/TaskBuilder.java @@ -138,13 +138,13 @@ public class TaskBuilder { } /** - * Schedules this {@link Task} for execution. + * Builds a {@link Task}. * * @return the built {@link Task} */ @NotNull - public Task schedule() { - Task task = new Task( + public Task build() { + return new Task( this.schedulerManager, this.runnable, this.shutdown, @@ -152,21 +152,17 @@ public class TaskBuilder { this.repeat, this.isTransient, this.owningExtension); - if (this.shutdown) { - Int2ObjectMap shutdownTasks = this.schedulerManager.shutdownTasks; - synchronized (shutdownTasks) { - shutdownTasks.put(task.getId(), task); - } - if (owningExtension != null) { - this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, task); - } - } else { - Int2ObjectMap tasks = this.schedulerManager.tasks; - synchronized (tasks) { - tasks.put(task.getId(), task); - } - task.schedule(); - } + } + + /** + * Schedules this {@link Task} for execution. + * + * @return the scheduled {@link Task} + */ + @NotNull + public Task schedule() { + Task task = build(); + task.schedule(); return task; } } diff --git a/src/main/java/net/minestom/server/utils/UniqueIdUtils.java b/src/main/java/net/minestom/server/utils/UniqueIdUtils.java index 09f0c70df..242a6a1b5 100644 --- a/src/main/java/net/minestom/server/utils/UniqueIdUtils.java +++ b/src/main/java/net/minestom/server/utils/UniqueIdUtils.java @@ -1,14 +1,15 @@ package net.minestom.server.utils; -import java.util.Random; +import org.jetbrains.annotations.ApiStatus; + import java.util.UUID; import java.util.regex.Pattern; /** * An utilities class for {@link UUID}. */ +@ApiStatus.Internal public final class UniqueIdUtils { - public static final Pattern UNIQUE_ID_PATTERN = Pattern.compile("\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b"); /** @@ -20,12 +21,4 @@ public final class UniqueIdUtils { public static boolean isUniqueId(String input) { return input.matches(UNIQUE_ID_PATTERN.pattern()); } - - public static UUID createRandomUUID(Random random) { - long most = random.nextLong() & -61441L | 16384L; - long least = random.nextLong() & 4611686018427387903L | Long.MAX_VALUE; - - return new UUID(most, least); - } - } diff --git a/src/main/java/net/minestom/server/utils/binary/BinaryBuffer.java b/src/main/java/net/minestom/server/utils/binary/BinaryBuffer.java index d0a60d40e..4689f624b 100644 --- a/src/main/java/net/minestom/server/utils/binary/BinaryBuffer.java +++ b/src/main/java/net/minestom/server/utils/binary/BinaryBuffer.java @@ -123,16 +123,15 @@ public final class BinaryBuffer { return nioBuffer.position(reader).slice().limit(writer); } - public void writeChannel(WritableByteChannel channel) throws IOException { - var writeBuffer = asByteBuffer(readerOffset, writerOffset); - while (writeBuffer.position() != writeBuffer.limit()) { - final int count = channel.write(writeBuffer); - if (count == -1) { - // EOS - throw new IOException("Disconnected"); - } - this.readerOffset += count; + public boolean writeChannel(WritableByteChannel channel) throws IOException { + var writeBuffer = asByteBuffer(readerOffset, writerOffset - readerOffset); + final int count = channel.write(writeBuffer); + if (count == -1) { + // EOS + throw new IOException("Disconnected"); } + this.readerOffset += count; + return writeBuffer.limit() == writeBuffer.position(); } public void readChannel(ReadableByteChannel channel) throws IOException { diff --git a/src/main/java/net/minestom/server/utils/thread/ThreadBindingExecutor.java b/src/main/java/net/minestom/server/utils/thread/ThreadBindingExecutor.java new file mode 100644 index 000000000..800be9a8d --- /dev/null +++ b/src/main/java/net/minestom/server/utils/thread/ThreadBindingExecutor.java @@ -0,0 +1,92 @@ +package net.minestom.server.utils.thread; + +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Executor service which will always give the same thread to a given Runnable. + * Uses

Runnable#hashCode()
to determine the thread to assign. + */ +public class ThreadBindingExecutor extends AbstractExecutorService { + + private MinestomThread[] threadExecutors; + + /** + * Creates a non-local thread-binding executor + * + * @param nThreads the number of threads + * @param name the name of the thread pool + */ + public ThreadBindingExecutor(int nThreads, String name) { + this(nThreads, name, false); + } + + /** + * @param nThreads the number of threads + * @param name the name of the thread pool + * @param local set to true if this executor is only used inside a method and should *not* be kept in the internal list of executors + */ + public ThreadBindingExecutor(int nThreads, String name, boolean local) { + threadExecutors = new MinestomThread[nThreads]; + for (int i = 0; i < nThreads; i++) { + threadExecutors[i] = new MinestomThread(1, name, local); + } + } + + @Override + public void shutdown() { + for (MinestomThread t : threadExecutors) { + t.shutdown(); + } + } + + @NotNull + @Override + public List shutdownNow() { + List allTasks = new LinkedList<>(); + for (MinestomThread t : threadExecutors) { + allTasks.addAll(t.shutdownNow()); + } + return allTasks; + } + + @Override + public boolean isShutdown() { + for (MinestomThread t : threadExecutors) { + if(!t.isShutdown()) + return false; + } + return true; + } + + @Override + public boolean isTerminated() { + for (MinestomThread t : threadExecutors) { + if(!t.isShutdown()) + return false; + } + return true; + } + + @Override + public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { + boolean terminated = true; + for (MinestomThread t : threadExecutors) { + terminated &= t.awaitTermination(timeout, unit); + } + return terminated; + } + + @Override + public void execute(@NotNull Runnable command) { + int hash = command.hashCode(); + if(hash < 0) hash = -hash; + int bucket = hash % threadExecutors.length; + + threadExecutors[bucket].execute(command); + } +} diff --git a/src/test/java/demo/PlayerInit.java b/src/test/java/demo/PlayerInit.java index aa833918b..a6aa6da57 100644 --- a/src/test/java/demo/PlayerInit.java +++ b/src/test/java/demo/PlayerInit.java @@ -29,6 +29,7 @@ import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; +import net.minestom.server.item.metadata.BundleMeta; import net.minestom.server.monitoring.BenchmarkManager; import net.minestom.server.monitoring.TickMonitor; import net.minestom.server.utils.MathUtils; @@ -104,6 +105,14 @@ public class PlayerInit { .canDestroy(Set.of(Block.DIAMOND_ORE))) .build(); player.getInventory().addItemStack(itemStack); + + ItemStack bundle = ItemStack.builder(Material.BUNDLE) + .meta(BundleMeta.class, bundleMetaBuilder -> { + bundleMetaBuilder.addItem(ItemStack.of(Material.DIAMOND, 5)); + bundleMetaBuilder.addItem(ItemStack.of(Material.RABBIT_FOOT, 5)); + }) + .build(); + player.getInventory().addItemStack(bundle); }); static { diff --git a/src/test/java/demo/commands/SummonCommand.java b/src/test/java/demo/commands/SummonCommand.java index 3440476bb..2ef48afa7 100644 --- a/src/test/java/demo/commands/SummonCommand.java +++ b/src/test/java/demo/commands/SummonCommand.java @@ -3,32 +3,39 @@ package demo.commands; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentEnum; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentEntityType; import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3; import net.minestom.server.command.builder.condition.Conditions; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.utils.location.RelativeVec; import org.jetbrains.annotations.NotNull; public class SummonCommand extends Command { private final ArgumentEntityType entity; - private final ArgumentRelativeVec3 pos; - private final ArgumentEnum entityClass; + private final Argument pos; + private final Argument entityClass; public SummonCommand() { super("summon"); setCondition(Conditions::playerOnly); entity = ArgumentType.EntityType("entity type"); - pos = ArgumentType.RelativeVec3("pos"); - entityClass = ArgumentType.Enum("class", EntityClass.class); - entityClass.setFormat(ArgumentEnum.Format.LOWER_CASED); - entityClass.setDefaultValue(EntityClass.CREATURE); + pos = ArgumentType.RelativeVec3("pos").setDefaultValue(() -> new RelativeVec( + new Vec(0, 0, 0), + RelativeVec.CoordinateType.RELATIVE, + true, true, true + )); + entityClass = ArgumentType.Enum("class", EntityClass.class) + .setFormat(ArgumentEnum.Format.LOWER_CASED) + .setDefaultValue(EntityClass.CREATURE);; addSyntax(this::execute, entity, pos, entityClass); setDefaultExecutor((sender, context) -> sender.sendMessage("Usage: /summon ")); }