Merge branch 'master' into viewable-broadcast

# Conflicts:
#	src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java
This commit is contained in:
TheMode 2021-09-02 18:50:55 +02:00
commit 4b2bda15f5
43 changed files with 791 additions and 423 deletions

View File

@ -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"

View File

@ -4,6 +4,7 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.map.Framebuffer; import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.MapColors; import net.minestom.server.map.MapColors;
import net.minestom.server.timer.Task; import net.minestom.server.timer.Task;
import net.minestom.server.utils.thread.ThreadBindingExecutor;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
import org.lwjgl.PointerBuffer; import org.lwjgl.PointerBuffer;
import org.lwjgl.glfw.GLFWErrorCallback; import org.lwjgl.glfw.GLFWErrorCallback;
@ -27,6 +28,8 @@ public abstract class GLFWCapableBuffer {
private final ByteBuffer colorsBuffer; private final ByteBuffer colorsBuffer;
private boolean onlyMapColors; private boolean onlyMapColors;
private static ThreadBindingExecutor threadBindingPool;
protected GLFWCapableBuffer(int width, int height) { protected GLFWCapableBuffer(int width, int height) {
this(width, height, GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API); 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."); 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() { public GLFWCapableBuffer unbindContextFromThread() {
@ -80,14 +89,17 @@ public abstract class GLFWCapableBuffer {
return MinecraftServer.getSchedulerManager() return MinecraftServer.getSchedulerManager()
.buildTask(new Runnable() { .buildTask(new Runnable() {
private boolean first = true; private boolean first = true;
private final Runnable subAction = () -> {
@Override
public void run() {
if(first) { if(first) {
changeRenderingThreadToCurrent(); changeRenderingThreadToCurrent();
first = false; first = false;
} }
render(rendering); render(rendering);
};
@Override
public void run() {
threadBindingPool.execute(subAction);
} }
}) })
.repeat(period) .repeat(period)

View File

@ -1,10 +1,8 @@
package net.minestom.server.attribute; package net.minestom.server.attribute;
import net.minestom.server.utils.UniqueIdUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/** /**
* Represent an attribute modifier. * Represent an attribute modifier.
@ -24,7 +22,7 @@ public class AttributeModifier {
* @param operation the operation to apply this modifier with * @param operation the operation to apply this modifier with
*/ */
public AttributeModifier(@NotNull String name, float amount, @NotNull AttributeOperation operation) { public AttributeModifier(@NotNull String name, float amount, @NotNull AttributeOperation operation) {
this(UniqueIdUtils.createRandomUUID(ThreadLocalRandom.current()), name, amount, operation); this(UUID.randomUUID(), name, amount, operation);
} }
/** /**

View File

@ -7,7 +7,9 @@ import net.minestom.server.entity.Entity;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
@ -18,13 +20,42 @@ public class BoundingBox {
private final Entity entity; private final Entity entity;
private final double x, y, z; private final double x, y, z;
private volatile Pos lastPosition; private final CachedFace bottomFace = new CachedFace(() -> List.of(
private List<Vec> bottomFace; new Vec(getMinX(), getMinY(), getMinZ()),
private List<Vec> topFace; new Vec(getMaxX(), getMinY(), getMinZ()),
private List<Vec> leftFace; new Vec(getMaxX(), getMinY(), getMaxZ()),
private List<Vec> rightFace; new Vec(getMinX(), getMinY(), getMaxZ())
private List<Vec> frontFace; ));
private List<Vec> backFace; 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. * 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} * @return the points at the bottom of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getBottomFace() { public @NotNull List<Vec> getBottomFace() {
this.bottomFace = get(bottomFace, () -> return bottomFace.get();
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())));
return bottomFace;
} }
/** /**
@ -334,12 +360,7 @@ public class BoundingBox {
* @return the points at the top of the {@link BoundingBox} * @return the points at the top of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getTopFace() { public @NotNull List<Vec> getTopFace() {
this.topFace = get(topFace, () -> return topFace.get();
List.of(new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())));
return topFace;
} }
/** /**
@ -348,12 +369,7 @@ public class BoundingBox {
* @return the points on the left face of the {@link BoundingBox} * @return the points on the left face of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getLeftFace() { public @NotNull List<Vec> getLeftFace() {
this.leftFace = get(leftFace, () -> return leftFace.get();
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())));
return leftFace;
} }
/** /**
@ -362,12 +378,7 @@ public class BoundingBox {
* @return the points on the right face of the {@link BoundingBox} * @return the points on the right face of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getRightFace() { public @NotNull List<Vec> getRightFace() {
this.rightFace = get(rightFace, () -> return rightFace.get();
List.of(new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ())));
return rightFace;
} }
/** /**
@ -376,12 +387,7 @@ public class BoundingBox {
* @return the points at the front of the {@link BoundingBox} * @return the points at the front of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getFrontFace() { public @NotNull List<Vec> getFrontFace() {
this.frontFace = get(frontFace, () -> return frontFace.get();
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ())));
return frontFace;
} }
/** /**
@ -390,12 +396,7 @@ public class BoundingBox {
* @return the points at the back of the {@link BoundingBox} * @return the points at the back of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getBackFace() { public @NotNull List<Vec> getBackFace() {
this.backFace = get(backFace, () -> List.of( return backFace.get();
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())));
return backFace;
} }
@Override @Override
@ -410,18 +411,42 @@ public class BoundingBox {
return result; return result;
} }
private @NotNull List<Vec> get(@Nullable List<Vec> face, Supplier<? extends List<Vec>> 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 { private enum Axis {
X, Y, Z X, Y, Z
} }
private class CachedFace {
private final AtomicReference<@Nullable PositionedPoints> reference = new AtomicReference<>(null);
private final Supplier<@NotNull List<Vec>> faceProducer;
private CachedFace(Supplier<@NotNull List<Vec>> faceProducer) {
this.faceProducer = faceProducer;
}
@NotNull List<Vec> 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<Vec> points;
private PositionedPoints(@NotNull Pos lastPosition, @NotNull List<Vec> points) {
this.lastPosition = lastPosition;
this.points = points;
}
}
} }

View File

@ -23,7 +23,7 @@ public class ArgumentEntityType extends ArgumentRegistry<EntityType> {
@Override @Override
public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) { public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, true); DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, true);
argumentNode.parser = "minecraft:entity_summon"; argumentNode.parser = "minecraft:resource_location";
argumentNode.suggestionsType = SuggestionType.SUMMONABLE_ENTITIES.getIdentifier(); argumentNode.suggestionsType = SuggestionType.SUMMONABLE_ENTITIES.getIdentifier();
nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode}); nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode});

View File

@ -450,7 +450,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
update(time); update(time);
ticks++; ticks++;
EventDispatcher.call(new EntityTickEvent(this), GlobalHandles.ENTITY_TICK); GlobalHandles.ENTITY_TICK.call(new EntityTickEvent(this));
// remove expired effects // remove expired effects
effectTick(time); effectTick(time);
@ -883,16 +883,15 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
public void addPassenger(@NotNull Entity entity) { public void addPassenger(@NotNull Entity entity) {
Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance"); 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"); Check.stateCondition(entity == getVehicle(), "Cannot add the entity vehicle as a passenger");
final Entity vehicle = entity.getVehicle(); final Entity vehicle = entity.getVehicle();
if (vehicle != null) { if (vehicle != null) {
vehicle.removePassenger(entity); vehicle.removePassenger(entity);
} }
this.passengers.add(entity); this.passengers.add(entity);
entity.vehicle = this; entity.vehicle = this;
sendPacketToViewersAndSelf(getPassengersPacket()); 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) { public void removePassenger(@NotNull Entity entity) {
Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance"); 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; entity.vehicle = null;
sendPacketToViewersAndSelf(getPassengersPacket()); sendPacketToViewersAndSelf(getPassengersPacket());
if (entity instanceof Player) {
Player player = (Player) entity;
player.getPlayerConnection().sendPacket(new PlayerPositionAndLookPacket(player.getPosition(),
(byte) 0x00, player.getNextTeleportId(), true));
}
} }
/** /**

View File

@ -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.StrayMeta;
import net.minestom.server.entity.metadata.monster.skeleton.WitherSkeletonMeta; 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.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.ZombieMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombieVillagerMeta; import net.minestom.server.entity.metadata.monster.zombie.ZombieVillagerMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombifiedPiglinMeta; 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:guardian", GuardianMeta::new);
supplier.put("minecraft:hoglin", HoglinMeta::new); supplier.put("minecraft:hoglin", HoglinMeta::new);
supplier.put("minecraft:horse", HorseMeta::new); supplier.put("minecraft:horse", HorseMeta::new);
supplier.put("minecraft:husk", HuskMeta::new);
supplier.put("minecraft:illusioner", IllusionerMeta::new); supplier.put("minecraft:illusioner", IllusionerMeta::new);
supplier.put("minecraft:iron_golem", IronGolemMeta::new); supplier.put("minecraft:iron_golem", IronGolemMeta::new);
supplier.put("minecraft:item", ItemEntityMeta::new); supplier.put("minecraft:item", ItemEntityMeta::new);

View File

@ -351,7 +351,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} }
// Tick event // Tick event
EventDispatcher.call(new PlayerTickEvent(this), GlobalHandles.PLAYER_TICK); GlobalHandles.PLAYER_TICK.call(new PlayerTickEvent(this));
} }
@Override @Override
@ -418,6 +418,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
getPlayerConnection().sendPacket(respawnPacket); getPlayerConnection().sendPacket(respawnPacket);
PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this); PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this);
EventDispatcher.call(respawnEvent); EventDispatcher.call(respawnEvent);
triggerStatus((byte) (24 + permissionLevel)); // Set permission level
refreshIsDead(false); refreshIsDead(false);
// Runnable called when teleportation is successful (after loading and sending necessary chunk) // 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) // Cannot load chunk (auto load is not enabled)
return; 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)); playerConnection.sendPacket(new UpdateViewPositionPacket(chunkX, chunkZ));
} }
public int getNextTeleportId() {
return teleportId.incrementAndGet();
}
public int getLastSentTeleportId() { public int getLastSentTeleportId() {
return teleportId.get(); return teleportId.get();
} }
@ -1567,7 +1576,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
@ApiStatus.Internal @ApiStatus.Internal
protected void synchronizePosition(boolean includeSelf) { protected void synchronizePosition(boolean includeSelf) {
if (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); super.synchronizePosition(includeSelf);
} }

View File

@ -10,10 +10,6 @@ public final class EventDispatcher {
MinecraftServer.getGlobalEventHandler().call(event); MinecraftServer.getGlobalEventHandler().call(event);
} }
public static <E extends Event> void call(@NotNull E event, @NotNull ListenerHandle<E> handle) {
MinecraftServer.getGlobalEventHandler().call(event, handle);
}
public static <E extends Event> ListenerHandle<E> getHandle(@NotNull Class<E> handleType) { public static <E extends Event> ListenerHandle<E> getHandle(@NotNull Class<E> handleType) {
return MinecraftServer.getGlobalEventHandler().getHandle(handleType); return MinecraftServer.getGlobalEventHandler().getHandle(handleType);
} }

View File

@ -188,22 +188,9 @@ public interface EventNode<T extends Event> {
*/ */
default void call(@NotNull T event) { default void call(@NotNull T event) {
//noinspection unchecked //noinspection unchecked
call(event, getHandle((Class<T>) event.getClass())); getHandle((Class<T>) event.getClass()).call(event);
} }
/**
* Calls an event starting from this node.
* <p>
* 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 <E> the event type
* @throws IllegalArgumentException if {@code handle}'s owner is not {@code this}
*/
<E extends T> void call(@NotNull E event, @NotNull ListenerHandle<E> handle);
/** /**
* Gets the handle of an event type. * Gets the handle of an event type.
* *
@ -211,21 +198,9 @@ public interface EventNode<T extends Event> {
* @param <E> the event type * @param <E> the event type
* @return the handle linked to {@code handleType} * @return the handle linked to {@code handleType}
*/ */
@ApiStatus.Experimental
<E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType); <E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType);
/**
* Gets if any listener has been registered for the given handle.
* May trigger an update if the cached data is not correct.
* <p>
* 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<? extends T> handle);
/** /**
* Execute a cancellable event with a callback to execute if the event is successful. * 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)}. * Event conditions and propagation is the same as {@link #call(Event)}.

View File

@ -39,18 +39,6 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
this.eventType = filter.eventType(); this.eventType = filter.eventType();
} }
@Override
public <E extends T> void call(@NotNull E event, @NotNull ListenerHandle<E> handle) {
final Handle<T> castedHandle = (Handle<T>) handle;
Check.argCondition(castedHandle.node != this, "Invalid handle owner");
if (!castedHandle.updated) castedHandle.update();
final List<Consumer<T>> listeners = castedHandle.listeners;
if (listeners.isEmpty()) return;
for (Consumer<T> listener : listeners) {
listener.accept(event);
}
}
@Override @Override
public <E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType) { public <E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType) {
//noinspection unchecked //noinspection unchecked
@ -58,13 +46,6 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
aClass -> new Handle<>(this, (Class<T>) aClass)); aClass -> new Handle<>(this, (Class<T>) aClass));
} }
@Override
public boolean hasListener(@NotNull ListenerHandle<? extends T> handle) {
final Handle<T> castedHandle = (Handle<T>) handle;
if (!castedHandle.updated) castedHandle.update();
return !castedHandle.listeners.isEmpty();
}
@Override @Override
public <E extends T> @NotNull List<EventNode<E>> findChildren(@NotNull String name, Class<E> eventType) { public <E extends T> @NotNull List<EventNode<E>> findChildren(@NotNull String name, Class<E> eventType) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
@ -239,6 +220,10 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
for (Class<? extends T> eventType : listenerMap.keySet()) { for (Class<? extends T> eventType : listenerMap.keySet()) {
node.invalidateEvent(eventType); node.invalidateEvent(eventType);
} }
// TODO bindings?
for (EventNodeImpl<T> child : children) {
child.invalidateEventsFor(node);
}
} }
private void invalidateEvent(Class<? extends T> eventClass) { private void invalidateEvent(Class<? extends T> eventClass) {
@ -277,7 +262,7 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
private static final class Handle<E extends Event> implements ListenerHandle<E> { private static final class Handle<E extends Event> implements ListenerHandle<E> {
private final EventNodeImpl<E> node; private final EventNodeImpl<E> node;
private final Class<E> eventType; private final Class<E> eventType;
private final List<Consumer<E>> listeners = new CopyOnWriteArrayList<>(); private Consumer<E> listener = null;
private volatile boolean updated; private volatile boolean updated;
Handle(EventNodeImpl<E> node, Class<E> eventType) { Handle(EventNodeImpl<E> node, Class<E> eventType) {
@ -285,145 +270,170 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
this.eventType = eventType; this.eventType = eventType;
} }
void update() { @Override
synchronized (GLOBAL_CHILD_LOCK) { public void call(@NotNull E event) {
this.listeners.clear(); final Consumer<E> listener = updatedListener();
recursiveUpdate(node); if (listener == null) return;
this.updated = true; try {
listener.accept(event);
} catch (Throwable e) {
MinecraftServer.getExceptionManager().handleException(e);
} }
} }
private void recursiveUpdate(EventNodeImpl<E> targetNode) { @Override
public boolean hasListener() {
return updatedListener() != null;
}
@Nullable Consumer<E> updatedListener() {
if (updated) return listener;
synchronized (GLOBAL_CHILD_LOCK) {
if (updated) return listener;
final Consumer<E> listener = createConsumer();
this.listener = listener;
this.updated = true;
return listener;
}
}
private @Nullable Consumer<E> createConsumer() {
// Standalone listeners // Standalone listeners
List<Consumer<E>> listeners = new ArrayList<>();
forTargetEvents(eventType, type -> { forTargetEvents(eventType, type -> {
final ListenerEntry<E> entry = targetNode.listenerMap.get(type); final ListenerEntry<E> entry = node.listenerMap.get(type);
if (entry != null) appendEntries(entry, targetNode); if (entry != null) {
final Consumer<E> result = listenersConsumer(entry);
if (result != null) listeners.add(result);
}
}); });
// Mapped nodes final Consumer<E>[] listenersArray = listeners.toArray(Consumer[]::new);
handleMappedNode(targetNode); // Mapped
// Add children final Consumer<E> mappedListener = mappedConsumer();
final Set<EventNodeImpl<E>> children = targetNode.children; // Children
if (children.isEmpty()) return; final Consumer<E>[] childrenListeners = node.children.stream()
children.stream()
.filter(child -> child.eventType.isAssignableFrom(eventType)) // Invalid event type .filter(child -> child.eventType.isAssignableFrom(eventType)) // Invalid event type
.sorted(Comparator.comparing(EventNode::getPriority)) .sorted(Comparator.comparing(EventNode::getPriority))
.forEach(this::recursiveUpdate); .map(child -> ((Handle<E>) child.getHandle(eventType)).updatedListener())
.filter(Objects::nonNull)
.toArray(Consumer[]::new);
// Empty check
final BiPredicate<E, Object> predicate = node.predicate;
final EventFilter<E, ?> 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<E> listener : listenersArray) {
listener.accept(e);
}
}
// Mapped nodes
if (hasMap) mappedListener.accept(e);
// Children
if (hasChildren) {
for (Consumer<E> 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)}.
* <p>
* Most computation should ideally be done outside the consumers as a one-time cost.
*/
private @Nullable Consumer<E> listenersConsumer(@NotNull ListenerEntry<E> entry) {
final EventListener<E>[] listenersCopy = entry.listeners.toArray(EventListener[]::new);
final Consumer<E>[] 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<E> listener = listenersCopy[0];
return e -> callListener(listener, e);
}
// Worse case scenario, try to run everything
return e -> {
if (!listenersEmpty) {
for (EventListener<E> listener : listenersCopy) {
callListener(listener, e);
}
}
if (!bindingsEmpty) {
for (Consumer<E> eConsumer : bindingsCopy) {
eConsumer.accept(e);
}
}
};
}
/**
* Create a consumer handling {@link EventNode#map(EventNode, Object)}.
* The goal is to limit the amount of map lookup. * The goal is to limit the amount of map lookup.
*/ */
private void handleMappedNode(EventNodeImpl<E> targetNode) { private @Nullable Consumer<E> mappedConsumer() {
final var mappedNodeCache = targetNode.mappedNodeCache; final var mappedNodeCache = node.mappedNodeCache;
if (mappedNodeCache.isEmpty()) return; if (mappedNodeCache.isEmpty()) return null;
Set<EventFilter<E, ?>> filters = new HashSet<>(mappedNodeCache.size()); Set<EventFilter<E, ?>> filters = new HashSet<>(mappedNodeCache.size());
Map<Object, Handle<E>> handlers = new HashMap<>(mappedNodeCache.size()); Map<Object, Handle<E>> handlers = new HashMap<>(mappedNodeCache.size());
// Retrieve all filters used to retrieve potential handlers // Retrieve all filters used to retrieve potential handlers
for (var mappedEntry : mappedNodeCache.entrySet()) { for (var mappedEntry : mappedNodeCache.entrySet()) {
final EventNodeImpl<E> mappedNode = mappedEntry.getValue(); final EventNodeImpl<E> mappedNode = mappedEntry.getValue();
final Handle<E> handle = (Handle<E>) mappedNode.getHandle(eventType); final Handle<E> handle = (Handle<E>) mappedNode.getHandle(eventType);
if (!mappedNode.hasListener(handle)) continue; // Implicit update if (!handle.hasListener()) continue; // Implicit update
filters.add(mappedNode.filter); filters.add(mappedNode.filter);
handlers.put(mappedEntry.getKey(), handle); handlers.put(mappedEntry.getKey(), handle);
} }
// If at least one mapped node listen to this handle type, // If at least one mapped node listen to this handle type,
// loop through them and forward to mapped node if there is a match // loop through them and forward to mapped node if there is a match
if (!filters.isEmpty()) { if (filters.isEmpty()) return null;
final var filterList = List.copyOf(filters); final EventFilter<E, ?>[] filterList = filters.toArray(EventFilter[]::new);
final int size = filterList.size(); final BiConsumer<EventFilter<E, ?>, E> mapper = (filter, event) -> {
final BiConsumer<EventFilter<E, ?>, E> mapper = (filter, event) -> { final Object handler = filter.castHandler(event);
final Object handler = filter.castHandler(event); final Handle<E> handle = handlers.get(handler);
final Handle<E> handle = handlers.get(handler); if (handle != null) handle.call(event);
if (handle != null) { // Run the listeners of the mapped node };
if (!handle.updated) handle.update(); if (filterList.length == 1) {
for (Consumer<E> listener : handle.listeners) { final var firstFilter = filterList[0];
listener.accept(event); // 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);
}
});
}
} }
} }
/** void callListener(@NotNull EventListener<E> listener, E event) {
* Add listeners from {@link EventNode#addListener(EventListener)} and EventListener.Result result = listener.run(event);
* {@link EventNode#register(EventBinding)} to the handle list.
* <p>
* Most computation should ideally be done outside the consumers as a one-time cost.
*/
private void appendEntries(ListenerEntry<E> entry, EventNodeImpl<E> targetNode) {
final var filter = targetNode.filter;
final var predicate = targetNode.predicate;
final boolean hasPredicate = predicate != null;
final List<EventListener<E>> listenersCopy = List.copyOf(entry.listeners);
final List<Consumer<E>> 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<E> 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<E> listener : listenersCopy) {
callListener(targetNode, listener, e);
}
}
if (!bindingsCopy.isEmpty()) {
for (Consumer<E> eConsumer : bindingsCopy) {
try {
eConsumer.accept(e);
} catch (Throwable t) {
MinecraftServer.getExceptionManager().handleException(t);
}
}
}
});
}
static <E extends Event> void callListener(EventNodeImpl<E> targetNode, EventListener<E> listener, E event) {
EventListener.Result result;
try {
result = listener.run(event);
} catch (Throwable t) {
result = EventListener.Result.EXCEPTION;
MinecraftServer.getExceptionManager().handleException(t);
}
if (result == EventListener.Result.EXPIRED) { if (result == EventListener.Result.EXPIRED) {
targetNode.removeListener(listener); node.removeListener(listener);
this.updated = false;
} }
} }
} }

View File

@ -1,13 +1,38 @@
package net.minestom.server.event; package net.minestom.server.event;
import org.jetbrains.annotations.ApiStatus; 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. * Useful to avoid map lookups.
* <p>
* It is recommended to store instances of this class in {@code static final} fields.
* *
* @param <E> the event type * @param <E> the event type
*/ */
@ApiStatus.Experimental
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
public interface ListenerHandle<E extends Event> { public interface ListenerHandle<E extends Event> {
/**
* Calls the given event.
* Will try to fast exit the execution when possible if {@link #hasListener()} return {@code false}.
* <p>
* 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.
* <p>
* 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();
} }

View File

@ -15,6 +15,7 @@ public final class ExceptionManager {
* @param e the occurred exception * @param e the occurred exception
*/ */
public void handleException(Throwable e) { public void handleException(Throwable e) {
// TODO handle OOM exceptions
this.getExceptionHandler().handleException(e); this.getExceptionHandler().handleException(e);
} }

View File

@ -280,7 +280,7 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
if (result) { if (result) {
PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ); PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ);
EventDispatcher.call(playerChunkLoadEvent, GlobalHandles.PLAYER_CHUNK_LOAD); GlobalHandles.PLAYER_CHUNK_LOAD.call(playerChunkLoadEvent);
} }
return result; return result;

View File

@ -9,13 +9,13 @@ import net.minestom.server.entity.Player;
import net.minestom.server.entity.pathfinding.PFBlock; import net.minestom.server.entity.pathfinding.PFBlock;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler; 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.FramedPacket;
import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.network.packet.server.play.UpdateLightPacket; import net.minestom.server.network.packet.server.play.UpdateLightPacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection; import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.world.biomes.Biome; import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -40,10 +40,8 @@ public class DynamicChunk extends Chunk {
protected final Int2ObjectOpenHashMap<Block> tickableMap = new Int2ObjectOpenHashMap<>(); protected final Int2ObjectOpenHashMap<Block> tickableMap = new Int2ObjectOpenHashMap<>();
private volatile long lastChangeTime; private volatile long lastChangeTime;
private final CachedPacket chunkCache = new CachedPacket(this::createChunkPacket);
private FramedPacket cachedChunkBuffer; private final CachedPacket lightCache = new CachedPacket(this::createLightPacket);
private FramedPacket cachedLightBuffer;
private long cachedPacketTime;
public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ) { public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ) {
super(instance, biomes, chunkX, chunkZ, true); super(instance, biomes, chunkX, chunkZ, true);
@ -126,34 +124,31 @@ public class DynamicChunk extends Chunk {
} }
@Override @Override
public synchronized void sendChunk(@NotNull Player player) { public void sendChunk(@NotNull Player player) {
if (!isLoaded()) return; if (!isLoaded()) return;
final PlayerConnection connection = player.getPlayerConnection(); 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) { 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; PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
socketConnection.write(lightPacket); socketConnection.write(lightPacket.body());
socketConnection.write(chunkPacket); socketConnection.write(chunkPacket.body());
} else { } else {
connection.sendPacket(createLightPacket()); connection.sendPacket(lightPacket.packet());
connection.sendPacket(createChunkPacket()); connection.sendPacket(chunkPacket.packet());
} }
} }
@Override @Override
public synchronized void sendChunk() { public void sendChunk() {
if (!isLoaded()) return; if (!isLoaded()) return;
sendPacketToViewers(createLightPacket()); if (getViewers().isEmpty()) return;
sendPacketToViewers(createChunkPacket()); final long lastChange = getLastChangeTime();
final FramedPacket lightPacket = lightCache.retrieveFramedPacket(lastChange);
final FramedPacket chunkPacket = chunkCache.retrieveFramedPacket(lastChange);
sendPacketToViewers(lightPacket.packet());
sendPacketToViewers(chunkPacket.packet());
} }
@NotNull @NotNull

View File

@ -139,8 +139,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta
} }
@ApiStatus.Internal @ApiStatus.Internal
public abstract boolean placeBlock(@NotNull Player player, @NotNull Block block, @NotNull Point blockPosition, public abstract boolean placeBlock(@NotNull BlockHandler.Placement placement);
@NotNull BlockFace blockFace, float cursorX, float cursorY, float cursorZ);
/** /**
* Does call {@link net.minestom.server.event.player.PlayerBlockBreakEvent} * Does call {@link net.minestom.server.event.player.PlayerBlockBreakEvent}
@ -732,7 +731,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta
// Tick event // Tick event
{ {
// Process tick events // 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 // Set last tick age
this.lastTickAge = time; this.lastTickAge = time;
} }

View File

@ -13,7 +13,6 @@ import net.minestom.server.event.instance.InstanceChunkUnloadEvent;
import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.instance.batch.ChunkGenerationBatch; import net.minestom.server.instance.batch.ChunkGenerationBatch;
import net.minestom.server.instance.block.Block; 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.BlockHandler;
import net.minestom.server.instance.block.rule.BlockPlacementRule; import net.minestom.server.instance.block.rule.BlockPlacementRule;
import net.minestom.server.network.packet.server.play.BlockChangePacket; 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.utils.validate.Check;
import net.minestom.server.world.DimensionType; import net.minestom.server.world.DimensionType;
import net.minestom.server.world.biomes.Biome; import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; 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 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) private long lastBlockChangeTime; // Time at which the last block change happened (#setBlock)
/** @ApiStatus.Experimental
* Creates an {@link InstanceContainer}. public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) {
*
* @param uniqueId the unique id of the instance
* @param dimensionType the dimension type of the instance
*/
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) {
super(uniqueId, dimensionType); super(uniqueId, dimensionType);
// Set the default chunk supplier using DynamicChunk
setChunkSupplier(DynamicChunk::new); setChunkSupplier(DynamicChunk::new);
// Set the default chunk loader which use the Anvil format setChunkLoader(Objects.requireNonNullElseGet(loader, () -> new AnvilLoader("world")));
setChunkLoader(new AnvilLoader("world"));
this.chunkLoader.loadInstance(this); this.chunkLoader.loadInstance(this);
} }
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) {
this(uniqueId, dimensionType, null);
}
@Override @Override
public void setBlock(int x, int y, int z, @NotNull Block block) { public void setBlock(int x, int y, int z, @NotNull Block block) {
final Chunk chunk = getChunkAt(x, z); final Chunk chunk = getChunkAt(x, z);
@ -156,12 +153,12 @@ public class InstanceContainer extends Instance {
} }
@Override @Override
public boolean placeBlock(@NotNull Player player, @NotNull Block block, @NotNull Point blockPosition, public boolean placeBlock(@NotNull BlockHandler.Placement placement) {
@NotNull BlockFace blockFace, float cursorX, float cursorY, float cursorZ) { final Point blockPosition = placement.getBlockPosition();
final Chunk chunk = getChunkAt(blockPosition); final Chunk chunk = getChunkAt(blockPosition);
if (!ChunkUtils.isLoaded(chunk)) return false; if (!ChunkUtils.isLoaded(chunk)) return false;
UNSAFE_setBlock(chunk, blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(), block, UNSAFE_setBlock(chunk, blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(),
new BlockHandler.PlayerPlacement(block, this, blockPosition, player, blockFace, cursorX, cursorY, cursorZ), null); placement.getBlock(), placement, null);
return true; return true;
} }
@ -275,7 +272,7 @@ public class InstanceContainer extends Instance {
.whenComplete((chunk, throwable) -> { .whenComplete((chunk, throwable) -> {
// TODO run in the instance thread? // TODO run in the instance thread?
cacheChunk(chunk); 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) { synchronized (loadingChunks) {
this.loadingChunks.remove(ChunkUtils.getChunkIndex(chunk)); this.loadingChunks.remove(ChunkUtils.getChunkIndex(chunk));
} }

View File

@ -4,6 +4,7 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.storage.StorageLocation; import net.minestom.server.storage.StorageLocation;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.DimensionType; import net.minestom.server.world.DimensionType;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -39,21 +40,32 @@ public final class InstanceManager {
* with the specified {@link DimensionType} and {@link StorageLocation}. * with the specified {@link DimensionType} and {@link StorageLocation}.
* *
* @param dimensionType the {@link DimensionType} of the instance * @param dimensionType the {@link DimensionType} of the instance
* @param loader the chunk loader
* @return the created {@link InstanceContainer} * @return the created {@link InstanceContainer}
*/ */
public @NotNull InstanceContainer createInstanceContainer(@NotNull DimensionType dimensionType) { @ApiStatus.Experimental
final InstanceContainer instanceContainer = new InstanceContainer(UUID.randomUUID(), dimensionType); public @NotNull InstanceContainer createInstanceContainer(@NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) {
final InstanceContainer instanceContainer = new InstanceContainer(UUID.randomUUID(), dimensionType, loader);
registerInstance(instanceContainer); registerInstance(instanceContainer);
return 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}. * Creates and register an {@link InstanceContainer}.
* *
* @return the created {@link InstanceContainer} * @return the created {@link InstanceContainer}
*/ */
public @NotNull InstanceContainer createInstanceContainer() { public @NotNull InstanceContainer createInstanceContainer() {
return createInstanceContainer(DimensionType.OVERWORLD); return createInstanceContainer(DimensionType.OVERWORLD, null);
} }
/** /**

View File

@ -3,7 +3,7 @@ package net.minestom.server.instance;
import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.instance.block.Block; 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 org.jetbrains.annotations.NotNull;
import java.util.Collection; import java.util.Collection;
@ -28,9 +28,8 @@ public class SharedInstance extends Instance {
} }
@Override @Override
public boolean placeBlock(@NotNull Player player, @NotNull Block block, @NotNull Point blockPosition, public boolean placeBlock(@NotNull BlockHandler.Placement placement) {
@NotNull BlockFace blockFace, float cursorX, float cursorY, float cursorZ) { return instanceContainer.placeBlock(placement);
return instanceContainer.placeBlock(player, block, blockPosition, blockFace, cursorX, cursorY, cursorZ);
} }
@Override @Override

View File

@ -114,14 +114,16 @@ public interface BlockHandler {
final class PlayerPlacement extends Placement { final class PlayerPlacement extends Placement {
private final Player player; private final Player player;
private final Player.Hand hand;
private final BlockFace blockFace; private final BlockFace blockFace;
private final float cursorX, cursorY, cursorZ; private final float cursorX, cursorY, cursorZ;
@ApiStatus.Internal @ApiStatus.Internal
public PlayerPlacement(Block block, Instance instance, Point blockPosition, 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); super(block, instance, blockPosition);
this.player = player; this.player = player;
this.hand = hand;
this.blockFace = blockFace; this.blockFace = blockFace;
this.cursorX = cursorX; this.cursorX = cursorX;
this.cursorY = cursorY; this.cursorY = cursorY;
@ -132,6 +134,10 @@ public interface BlockHandler {
return player; return player;
} }
public @NotNull Player.Hand getHand() {
return hand;
}
public @NotNull BlockFace getBlockFace() { public @NotNull BlockFace getBlockFace() {
return blockFace; return blockFace;
} }

View File

@ -173,7 +173,7 @@ final class BlockImpl implements Block {
private Block compute(Map<String, String> properties) { private Block compute(Map<String, String> properties) {
Block block = propertyEntry.get(properties); Block block = propertyEntry.get(properties);
if (block == null) if (block == null)
throw new IllegalArgumentException("Invalid properties: " + properties); throw new IllegalArgumentException("Invalid properties: " + properties + " for block " + this);
return nbt == null && handler == null ? block : return nbt == null && handler == null ? block :
new BlockImpl(block.registry(), propertyEntry, block.properties(), nbt, handler); new BlockImpl(block.registry(), propertyEntry, block.properties(), nbt, handler);
} }

View File

@ -401,18 +401,7 @@ public class Inventory extends AbstractInventory implements Viewable {
final InventoryClickResult clickResult = clickProcessor.dragging(player, final InventoryClickResult clickResult = clickProcessor.dragging(player,
slot != -999 ? (isInWindow ? this : playerInventory) : null, slot != -999 ? (isInWindow ? this : playerInventory) : null,
clickSlot, button, clickSlot, button,
clicked, cursor, 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);
}
});
if (clickResult == null || clickResult.isCancel()) { if (clickResult == null || clickResult.isCancel()) {
updateAll(player); updateAll(player);
return false; return false;

View File

@ -317,9 +317,7 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
final ItemStack cursor = getCursorItem(); final ItemStack cursor = getCursorItem();
final ItemStack clicked = slot != -999 ? getItemStackFromPacketSlot(slot) : ItemStack.AIR; final ItemStack clicked = slot != -999 ? getItemStackFromPacketSlot(slot) : ItemStack.AIR;
final InventoryClickResult clickResult = clickProcessor.dragging(player, this, final InventoryClickResult clickResult = clickProcessor.dragging(player, this,
slot, button, convertPlayerInventorySlot(slot, OFFSET), button, clicked, cursor);
clicked, cursor, this::getItemStackFromPacketSlot,
this::setItemStackFromPacketSlot);
if (clickResult == null || clickResult.isCancel()) { if (clickResult == null || clickResult.isCancel()) {
update(); update();
return false; return false;

View File

@ -1,8 +1,5 @@
package net.minestom.server.inventory.click; 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.EquipmentSlot;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventDispatcher;
@ -18,18 +15,18 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@ApiStatus.Internal @ApiStatus.Internal
public final class InventoryClickProcessor { public final class InventoryClickProcessor {
// Dragging maps // Dragging maps
private final Map<Player, IntSet> leftDraggingMap = new ConcurrentHashMap<>(); private final Map<Player, List<DragData>> leftDraggingMap = new ConcurrentHashMap<>();
private final Map<Player, IntSet> rightDraggingMap = new ConcurrentHashMap<>(); private final Map<Player, List<DragData>> rightDraggingMap = new ConcurrentHashMap<>();
public @NotNull InventoryClickResult leftClick(@NotNull Player player, @NotNull AbstractInventory inventory, public @NotNull InventoryClickResult leftClick(@NotNull Player player, @NotNull AbstractInventory inventory,
int slot, int slot,
@ -162,23 +159,21 @@ public final class InventoryClickProcessor {
public @Nullable InventoryClickResult dragging(@NotNull Player player, @Nullable AbstractInventory inventory, public @Nullable InventoryClickResult dragging(@NotNull Player player, @Nullable AbstractInventory inventory,
int slot, int button, int slot, int button,
@NotNull ItemStack clicked, @NotNull ItemStack cursor, @NotNull ItemStack clicked, @NotNull ItemStack cursor) {
@NotNull Int2ObjectFunction<ItemStack> itemGetter,
@NotNull BiConsumer<Integer, ItemStack> itemSetter) {
InventoryClickResult clickResult = null; InventoryClickResult clickResult = null;
final StackingRule stackingRule = cursor.getStackingRule(); final StackingRule stackingRule = cursor.getStackingRule();
if (slot != -999) { if (slot != -999) {
// Add slot // Add slot
if (button == 1) { if (button == 1) {
// Add left // Add left
IntSet left = leftDraggingMap.get(player); List<DragData> left = leftDraggingMap.get(player);
if (left == null) return null; if (left == null) return null;
left.add(slot); left.add(new DragData(slot, inventory));
} else if (button == 5) { } else if (button == 5) {
// Add right // Add right
IntSet right = rightDraggingMap.get(player); List<DragData> right = rightDraggingMap.get(player);
if (right == null) return null; if (right == null) return null;
right.add(slot); right.add(new DragData(slot, inventory));
} else if (button == 9) { } else if (button == 9) {
// Add middle // Add middle
// TODO // TODO
@ -188,18 +183,18 @@ public final class InventoryClickProcessor {
if (button == 0) { if (button == 0) {
// Start left // Start left
clickResult = startCondition(player, inventory, slot, ClickType.START_LEFT_DRAGGING, clicked, cursor); 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) { } else if (button == 2) {
// End left // End left
final IntSet slots = leftDraggingMap.remove(player); final List<DragData> slots = leftDraggingMap.remove(player);
if (slots == null) return null; if (slots == null) return null;
final int slotCount = slots.size(); final int slotCount = slots.size();
final int cursorAmount = stackingRule.getAmount(cursor); final int cursorAmount = stackingRule.getAmount(cursor);
if (slotCount > cursorAmount) return null; if (slotCount > cursorAmount) return null;
for (int s : slots) { for (DragData s : slots) {
// Apply each drag element // Apply each drag element
final ItemStack slotItem = itemGetter.apply(s); final ItemStack slotItem = s.inventory.getItemStack(s.slot);
clickResult = startCondition(player, inventory, s, ClickType.LEFT_DRAGGING, slotItem, cursor); clickResult = startCondition(player, s.inventory, s.slot, ClickType.LEFT_DRAGGING, slotItem, cursor);
if (clickResult.isCancel()) { if (clickResult.isCancel()) {
return clickResult; return clickResult;
} }
@ -210,8 +205,10 @@ public final class InventoryClickProcessor {
final int slotSize = (int) ((float) cursorAmount / (float) slotCount); final int slotSize = (int) ((float) cursorAmount / (float) slotCount);
// Place all waiting drag action // Place all waiting drag action
int finalCursorAmount = cursorAmount; int finalCursorAmount = cursorAmount;
for (int s : slots) { for (DragData dragData : slots) {
ItemStack slotItem = itemGetter.apply(s); final var inv = dragData.inventory;
final int s = dragData.slot;
ItemStack slotItem = inv.getItemStack(s);
final StackingRule slotItemRule = slotItem.getStackingRule(); final StackingRule slotItemRule = slotItem.getStackingRule();
final int amount = slotItemRule.getAmount(slotItem); final int amount = slotItemRule.getAmount(slotItem);
if (stackingRule.canBeStacked(cursor, slotItem)) { if (stackingRule.canBeStacked(cursor, slotItem)) {
@ -231,7 +228,7 @@ public final class InventoryClickProcessor {
slotItem = stackingRule.apply(cursor, slotSize); slotItem = stackingRule.apply(cursor, slotSize);
finalCursorAmount -= slotSize; finalCursorAmount -= slotSize;
} }
itemSetter.accept(s, slotItem); inv.setItemStack(s, slotItem);
callClickEvent(player, inventory, s, ClickType.LEFT_DRAGGING, slotItem, cursor); callClickEvent(player, inventory, s, ClickType.LEFT_DRAGGING, slotItem, cursor);
} }
// Update the cursor // Update the cursor
@ -239,18 +236,18 @@ public final class InventoryClickProcessor {
} else if (button == 4) { } else if (button == 4) {
// Start right // Start right
clickResult = startCondition(player, inventory, slot, ClickType.START_RIGHT_DRAGGING, clicked, cursor); 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) { } else if (button == 6) {
// End right // End right
final IntSet slots = rightDraggingMap.remove(player); final List<DragData> slots = rightDraggingMap.remove(player);
if (slots == null) return null; if (slots == null) return null;
final int size = slots.size(); final int size = slots.size();
int cursorAmount = stackingRule.getAmount(cursor); int cursorAmount = stackingRule.getAmount(cursor);
if (size > cursorAmount) return null; if (size > cursorAmount) return null;
// Verify if each slot can be modified (or cancel the whole drag) // Verify if each slot can be modified (or cancel the whole drag)
for (int s : slots) { for (DragData s : slots) {
ItemStack slotItem = itemGetter.apply(s); final ItemStack slotItem = s.inventory.getItemStack(s.slot);
clickResult = startCondition(player, inventory, s, ClickType.RIGHT_DRAGGING, slotItem, cursor); clickResult = startCondition(player, s.inventory, s.slot, ClickType.RIGHT_DRAGGING, slotItem, cursor);
if (clickResult.isCancel()) { if (clickResult.isCancel()) {
return clickResult; return clickResult;
} }
@ -259,8 +256,10 @@ public final class InventoryClickProcessor {
if (clickResult.isCancel()) return clickResult; if (clickResult.isCancel()) return clickResult;
// Place all waiting drag action // Place all waiting drag action
int finalCursorAmount = cursorAmount; int finalCursorAmount = cursorAmount;
for (int s : slots) { for (DragData dragData : slots) {
ItemStack slotItem = itemGetter.apply(s); final var inv = dragData.inventory;
final int s = dragData.slot;
ItemStack slotItem = inv.getItemStack(s);
StackingRule slotItemRule = slotItem.getStackingRule(); StackingRule slotItemRule = slotItem.getStackingRule();
if (stackingRule.canBeStacked(cursor, slotItem)) { if (stackingRule.canBeStacked(cursor, slotItem)) {
// Compatible item in the slot, increment by 1 // Compatible item in the slot, increment by 1
@ -274,7 +273,7 @@ public final class InventoryClickProcessor {
slotItem = stackingRule.apply(cursor, 1); slotItem = stackingRule.apply(cursor, 1);
finalCursorAmount -= 1; finalCursorAmount -= 1;
} }
itemSetter.accept(s, slotItem); inv.setItemStack(s, slotItem);
callClickEvent(player, inventory, s, ClickType.RIGHT_DRAGGING, slotItem, cursor); callClickEvent(player, inventory, s, ClickType.RIGHT_DRAGGING, slotItem, cursor);
} }
// Update the cursor // Update the cursor
@ -462,4 +461,14 @@ public final class InventoryClickProcessor {
this.leftDraggingMap.remove(player); this.leftDraggingMap.remove(player);
this.rightDraggingMap.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;
}
}
} }

View File

@ -7,6 +7,7 @@ import net.minestom.server.item.rule.VanillaStackingRule;
import net.minestom.server.tag.Tag; import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable; import net.minestom.server.tag.TagReadable;
import net.minestom.server.utils.NBTUtils; import net.minestom.server.utils.NBTUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -79,6 +80,24 @@ public final class ItemStack implements TagReadable, HoverEventSource<HoverEvent
return fromNBT(material, nbtCompound, 1); return fromNBT(material, nbtCompound, 1);
} }
/**
* Converts this item to an NBT tag containing the id (material), count (amount), and tag (meta).
*
* @param nbtCompound The nbt representation of the item
*/
@ApiStatus.Experimental
public static @NotNull ItemStack fromItemNBT(@NotNull NBTCompound nbtCompound) {
String id = nbtCompound.getString("id");
Check.notNull(id, "Item NBT must contain an id field.");
Material material = Material.fromNamespaceId(id);
Check.notNull(material, "Unknown material: {0}", id);
Byte amount = nbtCompound.getByte("Count");
return fromNBT(material,
nbtCompound.getCompound("tag"),
amount == null ? 1 : amount);
}
@Contract(pure = true) @Contract(pure = true)
public @NotNull Material getMaterial() { public @NotNull Material getMaterial() {
return material; return material;
@ -228,6 +247,20 @@ public final class ItemStack implements TagReadable, HoverEventSource<HoverEvent
NBTUtils.asBinaryTagHolder(this.meta.toNBT().getCompound("tag"))))); NBTUtils.asBinaryTagHolder(this.meta.toNBT().getCompound("tag")))));
} }
/**
* Converts this item to an NBT tag containing the id (material), count (amount), and tag (meta)
*
* @return The nbt representation of the item
*/
@ApiStatus.Experimental
public @NotNull NBTCompound toItemNBT() {
final NBTCompound nbtCompound = new NBTCompound();
nbtCompound.setString("id", getMaterial().name());
nbtCompound.setByte("Count", (byte) getAmount());
nbtCompound.set("tag", getMeta().toNBT());
return nbtCompound;
}
@Contract(value = "-> new", pure = true) @Contract(value = "-> new", pure = true)
private @NotNull ItemStackBuilder builder() { private @NotNull ItemStackBuilder builder() {
return new ItemStackBuilder(material, meta.builder()) return new ItemStackBuilder(material, meta.builder())

View File

@ -46,6 +46,7 @@ public class ItemStackBuilder {
MATERIAL_SUPPLIER_MAP.put(Material.FIREWORK_STAR, FireworkEffectMeta.Builder::new); MATERIAL_SUPPLIER_MAP.put(Material.FIREWORK_STAR, FireworkEffectMeta.Builder::new);
MATERIAL_SUPPLIER_MAP.put(Material.FIREWORK_ROCKET, FireworkMeta.Builder::new); MATERIAL_SUPPLIER_MAP.put(Material.FIREWORK_ROCKET, FireworkMeta.Builder::new);
MATERIAL_SUPPLIER_MAP.put(Material.PLAYER_HEAD, PlayerHeadMeta.Builder::new); MATERIAL_SUPPLIER_MAP.put(Material.PLAYER_HEAD, PlayerHeadMeta.Builder::new);
MATERIAL_SUPPLIER_MAP.put(Material.BUNDLE, BundleMeta.Builder::new);
MATERIAL_SUPPLIER_MAP.put(Material.LEATHER_HELMET, LeatherArmorMeta.Builder::new); MATERIAL_SUPPLIER_MAP.put(Material.LEATHER_HELMET, LeatherArmorMeta.Builder::new);
MATERIAL_SUPPLIER_MAP.put(Material.LEATHER_CHESTPLATE, LeatherArmorMeta.Builder::new); MATERIAL_SUPPLIER_MAP.put(Material.LEATHER_CHESTPLATE, LeatherArmorMeta.Builder::new);

View File

@ -0,0 +1,84 @@
package net.minestom.server.item.metadata;
import net.minestom.server.item.ItemMeta;
import net.minestom.server.item.ItemMetaBuilder;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.jglrxavpok.hephaistos.nbt.NBTList;
import org.jglrxavpok.hephaistos.nbt.NBTTypes;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
@ApiStatus.Experimental
public class BundleMeta extends ItemMeta implements ItemMetaBuilder.Provider<BundleMeta.Builder> {
private final List<ItemStack> items;
protected BundleMeta(ItemMetaBuilder metaBuilder,
@NotNull List<ItemStack> items) {
super(metaBuilder);
this.items = List.copyOf(items);
}
public @NotNull List<ItemStack> getItems() {
return items;
}
public static class Builder extends ItemMetaBuilder {
private List<ItemStack> items = new ArrayList<>();
public Builder items(@NotNull List<ItemStack> 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<NBTCompound> 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<NBTCompound> items = nbtCompound.getList("Items");
for (NBTCompound item : items) {
this.items.add(ItemStack.fromItemNBT(item));
}
}
}
@Override
protected @NotNull Supplier<@NotNull ItemMetaBuilder> getSupplier() {
return Builder::new;
}
}
}

View File

@ -157,8 +157,8 @@ public class BlockPlacementListener {
return; return;
} }
// Place the block // Place the block
instance.placeBlock(player, resultBlock, placementPosition, instance.placeBlock(new BlockHandler.PlayerPlacement(resultBlock, instance, placementPosition, player, hand, blockFace,
blockFace, packet.cursorPositionX, packet.cursorPositionY, packet.cursorPositionZ); packet.cursorPositionX, packet.cursorPositionY, packet.cursorPositionZ));
// Block consuming // Block consuming
if (playerBlockPlaceEvent.doesConsumeBlock()) { if (playerBlockPlaceEvent.doesConsumeBlock()) {
// Consume the block in the player's hand // Consume the block in the player's hand

View File

@ -2,11 +2,12 @@ package net.minestom.server.listener;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.GlobalHandles; import net.minestom.server.event.GlobalHandles;
import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.client.play.*; 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 net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -32,9 +33,9 @@ public class PlayerPositionListener {
player.refreshReceivedTeleportId(packet.teleportId); 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(); final var currentPosition = player.getPosition();
if (currentPosition.equals(newPosition)) { if (currentPosition.equals(packetPosition)) {
// For some reason, the position is the same // For some reason, the position is the same
return; return;
} }
@ -48,21 +49,37 @@ public class PlayerPositionListener {
return; return;
} }
// Try to move in an unloaded chunk, prevent it // 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); player.teleport(currentPosition);
return; return;
} }
PlayerMoveEvent playerMoveEvent = new PlayerMoveEvent(player, newPosition); PlayerMoveEvent playerMoveEvent = new PlayerMoveEvent(player, packetPosition);
EventDispatcher.call(playerMoveEvent, GlobalHandles.PLAYER_MOVE); GlobalHandles.PLAYER_MOVE.call(playerMoveEvent);
// True if the event call changed the player position (possibly a teleport) if (!currentPosition.equals(player.getPosition())) {
if (!playerMoveEvent.isCancelled() && currentPosition.equals(player.getPosition())) { // Player has been teleported in the event
// Move the player return;
player.refreshPosition(playerMoveEvent.getNewPosition()); }
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); player.refreshOnGround(onGround);
} else { } else {
// Cancelled, teleport to previous position // Position modified by the event
player.teleport(player.getPosition()); if (packetPosition.samePoint(eventPosition)) {
player.refreshPosition(eventPosition, true);
player.refreshOnGround(onGround);
player.setView(eventPosition.yaw(), eventPosition.pitch());
} else {
player.teleport(eventPosition);
}
} }
} }
} }

View File

@ -66,8 +66,6 @@ public class WindowListener {
successful = inventory.doubleClick(player, slot); 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 // Prevent ghost item when the click is cancelled
if (!successful) { if (!successful) {
player.getInventory().update(); player.getInventory().update();
@ -75,6 +73,10 @@ public class WindowListener {
((Inventory) inventory).update(player); ((Inventory) inventory).update(player);
} }
} }
// Prevent the player from picking a ghost item in cursor
refreshCursorItem(player, inventory);
// (Why is the ping packet necessary?) // (Why is the ping packet necessary?)
PingPacket pingPacket = new PingPacket(); PingPacket pingPacket = new PingPacket();
pingPacket.id = (1 << 30) | (windowId << 16); pingPacket.id = (1 << 30) | (windowId << 16);

View File

@ -2,7 +2,6 @@ package net.minestom.server.listener.manager;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.GlobalHandles; import net.minestom.server.event.GlobalHandles;
import net.minestom.server.event.player.PlayerPacketEvent; import net.minestom.server.event.player.PlayerPacketEvent;
import net.minestom.server.listener.*; import net.minestom.server.listener.*;
@ -90,7 +89,7 @@ public final class PacketListenerManager {
// Event // Event
PlayerPacketEvent playerPacketEvent = new PlayerPacketEvent(player, packet); PlayerPacketEvent playerPacketEvent = new PlayerPacketEvent(player, packet);
EventDispatcher.call(playerPacketEvent, GlobalHandles.PLAYER_PACKET); GlobalHandles.PLAYER_PACKET.call(playerPacketEvent);
if (playerPacketEvent.isCancelled()) { if (playerPacketEvent.isCancelled()) {
return; return;
} }

View File

@ -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<ServerPacket> supplier;
private volatile long packetTimestamp;
private SoftReference<FramedPacket> packet;
public CachedPacket(Supplier<ServerPacket> 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;
}
}

View File

@ -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.ComponentHoldingServerPacket;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.SetCompressionPacket; 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.network.socket.Worker;
import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Utils; import net.minestom.server.utils.Utils;
@ -32,10 +31,9 @@ import java.net.SocketAddress;
import java.nio.BufferUnderflowException; import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import java.util.Map; import java.util.*;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
/** /**
@ -46,6 +44,8 @@ import java.util.zip.DataFormatException;
@ApiStatus.Internal @ApiStatus.Internal
public class PlayerSocketConnection extends PlayerConnection { public class PlayerSocketConnection extends PlayerConnection {
private final static Logger LOGGER = LoggerFactory.getLogger(PlayerSocketConnection.class); private final static Logger LOGGER = LoggerFactory.getLogger(PlayerSocketConnection.class);
private final static Queue<BinaryBuffer> POOLED_BUFFERS = new ConcurrentLinkedQueue<>();
private final static int BUFFER_SIZE = 262_143;
private final Worker worker; private final Worker worker;
private final SocketChannel channel; private final SocketChannel channel;
@ -73,7 +73,9 @@ public class PlayerSocketConnection extends PlayerConnection {
private UUID bungeeUuid; private UUID bungeeUuid;
private PlayerSkin bungeeSkin; private PlayerSkin bungeeSkin;
private final BinaryBuffer tickBuffer = BinaryBuffer.ofSize(Server.SOCKET_BUFFER_SIZE); private final Object bufferLock = new Object();
private final List<BinaryBuffer> waitingBuffers = new ArrayList<>();
private BinaryBuffer tickBuffer = BinaryBuffer.ofSize(BUFFER_SIZE);
private volatile BinaryBuffer cacheBuffer; private volatile BinaryBuffer cacheBuffer;
public PlayerSocketConnection(@NotNull Worker worker, @NotNull SocketChannel channel, SocketAddress remoteAddress) { 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) { public void write(@NotNull ByteBuffer buffer) {
synchronized (tickBuffer) { synchronized (bufferLock) {
if (!tickBuffer.canWrite(buffer.position())) { if (!tickBuffer.canWrite(buffer.position())) {
// Tick buffer is full, flush before appending // Tick buffer is full, flush before appending
flush(); flush();
@ -246,7 +248,7 @@ public class PlayerSocketConnection extends PlayerConnection {
} }
public void writeAndFlush(@NotNull ServerPacket packet) { public void writeAndFlush(@NotNull ServerPacket packet) {
synchronized (tickBuffer) { synchronized (bufferLock) {
write(packet); write(packet);
flush(); flush();
} }
@ -255,31 +257,45 @@ public class PlayerSocketConnection extends PlayerConnection {
@Override @Override
public void flush() { public void flush() {
if (!channel.isOpen()) return; if (!channel.isOpen()) return;
synchronized (tickBuffer) { if (tickBuffer.readableBytes() == 0 && waitingBuffers.isEmpty()) return;
if (tickBuffer.readableBytes() == 0) return; synchronized (bufferLock) {
try { if (tickBuffer.readableBytes() == 0 && waitingBuffers.isEmpty()) return;
if (encrypted) { if (encrypted) {
final Cipher cipher = encryptCipher; final Cipher cipher = encryptCipher;
// Encrypt data first // Encrypt data first
final int remainingBytes = tickBuffer.readableBytes(); final int remainingBytes = tickBuffer.readableBytes();
final byte[] bytes = tickBuffer.readRemainingBytes(); final byte[] bytes = tickBuffer.readRemainingBytes();
byte[] outTempArray = new byte[cipher.getOutputSize(remainingBytes)]; byte[] outTempArray = new byte[cipher.getOutputSize(remainingBytes)];
try {
cipher.update(bytes, 0, remainingBytes, outTempArray); cipher.update(bytes, 0, remainingBytes, outTempArray);
this.tickBuffer.clear(); } catch (ShortBufferException e) {
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"))) {
MinecraftServer.getExceptionManager().handleException(e); MinecraftServer.getExceptionManager().handleException(e);
} }
} catch (ShortBufferException e) {
e.printStackTrace();
} finally {
this.tickBuffer.clear(); this.tickBuffer.clear();
this.tickBuffer.writeBytes(outTempArray);
} }
this.waitingBuffers.add(tickBuffer);
Iterator<BinaryBuffer> 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 @Override
public void disconnect() { public void disconnect() {
this.worker.disconnect(this, channel); this.worker.disconnect(this, channel);
synchronized (bufferLock) {
POOLED_BUFFERS.addAll(waitingBuffers);
}
} }
public @NotNull SocketChannel getChannel() { public @NotNull SocketChannel getChannel() {

View File

@ -18,8 +18,8 @@ public final class Server {
public static final Logger LOGGER = LoggerFactory.getLogger(Server.class); public static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
public static final int WORKER_COUNT = Integer.getInteger("minestom.workers", public static final int WORKER_COUNT = Integer.getInteger("minestom.workers",
Runtime.getRuntime().availableProcessors()); 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 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; public static final boolean NO_DELAY = true;
private volatile boolean stop; private volatile boolean stop;

View File

@ -84,6 +84,7 @@ public final class Worker extends Thread {
socket.setSendBufferSize(Server.SOCKET_BUFFER_SIZE); socket.setSendBufferSize(Server.SOCKET_BUFFER_SIZE);
socket.setReceiveBufferSize(Server.SOCKET_BUFFER_SIZE); socket.setReceiveBufferSize(Server.SOCKET_BUFFER_SIZE);
socket.setTcpNoDelay(Server.NO_DELAY); socket.setTcpNoDelay(Server.NO_DELAY);
socket.setSoTimeout(30 * 1000); // 30 seconds
this.selector.wakeup(); this.selector.wakeup();
} }

View File

@ -135,7 +135,7 @@ public abstract class ThreadProvider {
return; return;
try { try {
chunk.tick(time); chunk.tick(time);
} catch (Exception e) { } catch (Throwable e) {
MinecraftServer.getExceptionManager().handleException(e); MinecraftServer.getExceptionManager().handleException(e);
} }
final var entities = chunkEntry.entities; final var entities = chunkEntry.entities;
@ -148,7 +148,7 @@ public abstract class ThreadProvider {
} }
try { try {
entity.tick(time); entity.tick(time);
} catch (Exception e) { } catch (Throwable e) {
MinecraftServer.getExceptionManager().handleException(e); MinecraftServer.getExceptionManager().handleException(e);
} }
} }

View File

@ -100,12 +100,26 @@ public class Task implements Runnable {
* Sets up the task for correct execution. * Sets up the task for correct execution.
*/ */
public void schedule() { public void schedule() {
if(owningExtension != null) { if(this.shutdown) {
this.schedulerManager.onScheduleFromExtension(owningExtension, this); Int2ObjectMap<Task> shutdownTasks = this.schedulerManager.shutdownTasks;
synchronized (shutdownTasks) {
shutdownTasks.put(getId(), this);
}
if (owningExtension != null) {
this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, this);
}
} else {
Int2ObjectMap<Task> 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);
} }
/** /**

View File

@ -138,13 +138,13 @@ public class TaskBuilder {
} }
/** /**
* Schedules this {@link Task} for execution. * Builds a {@link Task}.
* *
* @return the built {@link Task} * @return the built {@link Task}
*/ */
@NotNull @NotNull
public Task schedule() { public Task build() {
Task task = new Task( return new Task(
this.schedulerManager, this.schedulerManager,
this.runnable, this.runnable,
this.shutdown, this.shutdown,
@ -152,21 +152,17 @@ public class TaskBuilder {
this.repeat, this.repeat,
this.isTransient, this.isTransient,
this.owningExtension); this.owningExtension);
if (this.shutdown) { }
Int2ObjectMap<Task> shutdownTasks = this.schedulerManager.shutdownTasks;
synchronized (shutdownTasks) { /**
shutdownTasks.put(task.getId(), task); * Schedules this {@link Task} for execution.
} *
if (owningExtension != null) { * @return the scheduled {@link Task}
this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, task); */
} @NotNull
} else { public Task schedule() {
Int2ObjectMap<Task> tasks = this.schedulerManager.tasks; Task task = build();
synchronized (tasks) { task.schedule();
tasks.put(task.getId(), task);
}
task.schedule();
}
return task; return task;
} }
} }

View File

@ -1,14 +1,15 @@
package net.minestom.server.utils; package net.minestom.server.utils;
import java.util.Random; import org.jetbrains.annotations.ApiStatus;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* An utilities class for {@link UUID}. * An utilities class for {@link UUID}.
*/ */
@ApiStatus.Internal
public final class UniqueIdUtils { 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"); 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) { public static boolean isUniqueId(String input) {
return input.matches(UNIQUE_ID_PATTERN.pattern()); 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);
}
} }

View File

@ -123,16 +123,15 @@ public final class BinaryBuffer {
return nioBuffer.position(reader).slice().limit(writer); return nioBuffer.position(reader).slice().limit(writer);
} }
public void writeChannel(WritableByteChannel channel) throws IOException { public boolean writeChannel(WritableByteChannel channel) throws IOException {
var writeBuffer = asByteBuffer(readerOffset, writerOffset); var writeBuffer = asByteBuffer(readerOffset, writerOffset - readerOffset);
while (writeBuffer.position() != writeBuffer.limit()) { final int count = channel.write(writeBuffer);
final int count = channel.write(writeBuffer); if (count == -1) {
if (count == -1) { // EOS
// EOS throw new IOException("Disconnected");
throw new IOException("Disconnected");
}
this.readerOffset += count;
} }
this.readerOffset += count;
return writeBuffer.limit() == writeBuffer.position();
} }
public void readChannel(ReadableByteChannel channel) throws IOException { public void readChannel(ReadableByteChannel channel) throws IOException {

View File

@ -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 <pre>Runnable#hashCode()</pre> 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<Runnable> shutdownNow() {
List<Runnable> 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);
}
}

View File

@ -29,6 +29,7 @@ import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType; import net.minestom.server.inventory.InventoryType;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
import net.minestom.server.item.metadata.BundleMeta;
import net.minestom.server.monitoring.BenchmarkManager; import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.monitoring.TickMonitor; import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
@ -104,6 +105,14 @@ public class PlayerInit {
.canDestroy(Set.of(Block.DIAMOND_ORE))) .canDestroy(Set.of(Block.DIAMOND_ORE)))
.build(); .build();
player.getInventory().addItemStack(itemStack); 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 { static {

View File

@ -3,32 +3,39 @@ package demo.commands;
import net.minestom.server.command.CommandSender; import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext; 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.ArgumentEnum;
import net.minestom.server.command.builder.arguments.ArgumentType; 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.minecraft.registry.ArgumentEntityType;
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3; import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3;
import net.minestom.server.command.builder.condition.Conditions; 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.Entity;
import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.LivingEntity;
import net.minestom.server.utils.location.RelativeVec;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class SummonCommand extends Command { public class SummonCommand extends Command {
private final ArgumentEntityType entity; private final ArgumentEntityType entity;
private final ArgumentRelativeVec3 pos; private final Argument<RelativeVec> pos;
private final ArgumentEnum<EntityClass> entityClass; private final Argument<EntityClass> entityClass;
public SummonCommand() { public SummonCommand() {
super("summon"); super("summon");
setCondition(Conditions::playerOnly); setCondition(Conditions::playerOnly);
entity = ArgumentType.EntityType("entity type"); entity = ArgumentType.EntityType("entity type");
pos = ArgumentType.RelativeVec3("pos"); pos = ArgumentType.RelativeVec3("pos").setDefaultValue(() -> new RelativeVec(
entityClass = ArgumentType.Enum("class", EntityClass.class); new Vec(0, 0, 0),
entityClass.setFormat(ArgumentEnum.Format.LOWER_CASED); RelativeVec.CoordinateType.RELATIVE,
entityClass.setDefaultValue(EntityClass.CREATURE); true, true, true
));
entityClass = ArgumentType.Enum("class", EntityClass.class)
.setFormat(ArgumentEnum.Format.LOWER_CASED)
.setDefaultValue(EntityClass.CREATURE);;
addSyntax(this::execute, entity, pos, entityClass); addSyntax(this::execute, entity, pos, entityClass);
setDefaultExecutor((sender, context) -> sender.sendMessage("Usage: /summon <type> <x> <y> <z> <class>")); setDefaultExecutor((sender, context) -> sender.sendMessage("Usage: /summon <type> <x> <y> <z> <class>"));
} }