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

View File

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

View File

@ -7,7 +7,9 @@ import net.minestom.server.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
/**
@ -18,13 +20,42 @@ public class BoundingBox {
private final Entity entity;
private final double x, y, z;
private volatile Pos lastPosition;
private List<Vec> bottomFace;
private List<Vec> topFace;
private List<Vec> leftFace;
private List<Vec> rightFace;
private List<Vec> frontFace;
private List<Vec> backFace;
private final CachedFace bottomFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())
));
private final CachedFace topFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())
));
private final CachedFace leftFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())
));
private final CachedFace rightFace = new CachedFace(() -> List.of(
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ())
));
private final CachedFace frontFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ())
));
private final CachedFace backFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())
));
/**
* Creates a {@link BoundingBox} linked to an {@link Entity} and with a specific size.
@ -320,12 +351,7 @@ public class BoundingBox {
* @return the points at the bottom of the {@link BoundingBox}
*/
public @NotNull List<Vec> getBottomFace() {
this.bottomFace = get(bottomFace, () ->
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())));
return bottomFace;
return bottomFace.get();
}
/**
@ -334,12 +360,7 @@ public class BoundingBox {
* @return the points at the top of the {@link BoundingBox}
*/
public @NotNull List<Vec> getTopFace() {
this.topFace = get(topFace, () ->
List.of(new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())));
return topFace;
return topFace.get();
}
/**
@ -348,12 +369,7 @@ public class BoundingBox {
* @return the points on the left face of the {@link BoundingBox}
*/
public @NotNull List<Vec> getLeftFace() {
this.leftFace = get(leftFace, () ->
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())));
return leftFace;
return leftFace.get();
}
/**
@ -362,12 +378,7 @@ public class BoundingBox {
* @return the points on the right face of the {@link BoundingBox}
*/
public @NotNull List<Vec> getRightFace() {
this.rightFace = get(rightFace, () ->
List.of(new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ())));
return rightFace;
return rightFace.get();
}
/**
@ -376,12 +387,7 @@ public class BoundingBox {
* @return the points at the front of the {@link BoundingBox}
*/
public @NotNull List<Vec> getFrontFace() {
this.frontFace = get(frontFace, () ->
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ())));
return frontFace;
return frontFace.get();
}
/**
@ -390,12 +396,7 @@ public class BoundingBox {
* @return the points at the back of the {@link BoundingBox}
*/
public @NotNull List<Vec> getBackFace() {
this.backFace = get(backFace, () -> List.of(
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())));
return backFace;
return backFace.get();
}
@Override
@ -410,18 +411,42 @@ public class BoundingBox {
return result;
}
private @NotNull List<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 {
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
public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, true);
argumentNode.parser = "minecraft:entity_summon";
argumentNode.parser = "minecraft:resource_location";
argumentNode.suggestionsType = SuggestionType.SUMMONABLE_ENTITIES.getIdentifier();
nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode});

View File

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

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.WitherSkeletonMeta;
import net.minestom.server.entity.metadata.monster.zombie.DrownedMeta;
import net.minestom.server.entity.metadata.monster.zombie.HuskMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombieMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombieVillagerMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombifiedPiglinMeta;
@ -119,6 +120,7 @@ final class EntityTypeImpl implements EntityType {
supplier.put("minecraft:guardian", GuardianMeta::new);
supplier.put("minecraft:hoglin", HoglinMeta::new);
supplier.put("minecraft:horse", HorseMeta::new);
supplier.put("minecraft:husk", HuskMeta::new);
supplier.put("minecraft:illusioner", IllusionerMeta::new);
supplier.put("minecraft:iron_golem", IronGolemMeta::new);
supplier.put("minecraft:item", ItemEntityMeta::new);

View File

@ -351,7 +351,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
}
// Tick event
EventDispatcher.call(new PlayerTickEvent(this), GlobalHandles.PLAYER_TICK);
GlobalHandles.PLAYER_TICK.call(new PlayerTickEvent(this));
}
@Override
@ -418,6 +418,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
getPlayerConnection().sendPacket(respawnPacket);
PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this);
EventDispatcher.call(respawnEvent);
triggerStatus((byte) (24 + permissionLevel)); // Set permission level
refreshIsDead(false);
// Runnable called when teleportation is successful (after loading and sending necessary chunk)
@ -1195,7 +1196,11 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Cannot load chunk (auto load is not enabled)
return;
}
chunk.addViewer(this);
try {
chunk.addViewer(this);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
});
});
}
@ -1548,6 +1553,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
playerConnection.sendPacket(new UpdateViewPositionPacket(chunkX, chunkZ));
}
public int getNextTeleportId() {
return teleportId.incrementAndGet();
}
public int getLastSentTeleportId() {
return teleportId.get();
}
@ -1567,7 +1576,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
@ApiStatus.Internal
protected void synchronizePosition(boolean includeSelf) {
if (includeSelf) {
playerConnection.sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, teleportId.incrementAndGet(), false));
playerConnection.sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, getNextTeleportId(), false));
}
super.synchronizePosition(includeSelf);
}

View File

@ -10,10 +10,6 @@ public final class EventDispatcher {
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) {
return MinecraftServer.getGlobalEventHandler().getHandle(handleType);
}

View File

@ -188,22 +188,9 @@ public interface EventNode<T extends Event> {
*/
default void call(@NotNull T event) {
//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.
*
@ -211,21 +198,9 @@ public interface EventNode<T extends Event> {
* @param <E> the event type
* @return the handle linked to {@code handleType}
*/
@ApiStatus.Experimental
<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.
* 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();
}
@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
public <E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType) {
//noinspection unchecked
@ -58,13 +46,6 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
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
public <E extends T> @NotNull List<EventNode<E>> findChildren(@NotNull String name, Class<E> eventType) {
synchronized (GLOBAL_CHILD_LOCK) {
@ -239,6 +220,10 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
for (Class<? extends T> eventType : listenerMap.keySet()) {
node.invalidateEvent(eventType);
}
// TODO bindings?
for (EventNodeImpl<T> child : children) {
child.invalidateEventsFor(node);
}
}
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 final EventNodeImpl<E> node;
private final Class<E> eventType;
private final List<Consumer<E>> listeners = new CopyOnWriteArrayList<>();
private Consumer<E> listener = null;
private volatile boolean updated;
Handle(EventNodeImpl<E> node, Class<E> eventType) {
@ -285,145 +270,170 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
this.eventType = eventType;
}
void update() {
synchronized (GLOBAL_CHILD_LOCK) {
this.listeners.clear();
recursiveUpdate(node);
this.updated = true;
@Override
public void call(@NotNull E event) {
final Consumer<E> listener = updatedListener();
if (listener == null) return;
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
List<Consumer<E>> listeners = new ArrayList<>();
forTargetEvents(eventType, type -> {
final ListenerEntry<E> entry = targetNode.listenerMap.get(type);
if (entry != null) appendEntries(entry, targetNode);
final ListenerEntry<E> entry = node.listenerMap.get(type);
if (entry != null) {
final Consumer<E> result = listenersConsumer(entry);
if (result != null) listeners.add(result);
}
});
// Mapped nodes
handleMappedNode(targetNode);
// Add children
final Set<EventNodeImpl<E>> children = targetNode.children;
if (children.isEmpty()) return;
children.stream()
final Consumer<E>[] listenersArray = listeners.toArray(Consumer[]::new);
// Mapped
final Consumer<E> mappedListener = mappedConsumer();
// Children
final Consumer<E>[] childrenListeners = node.children.stream()
.filter(child -> child.eventType.isAssignableFrom(eventType)) // Invalid event type
.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.
*/
private void handleMappedNode(EventNodeImpl<E> targetNode) {
final var mappedNodeCache = targetNode.mappedNodeCache;
if (mappedNodeCache.isEmpty()) return;
private @Nullable Consumer<E> mappedConsumer() {
final var mappedNodeCache = node.mappedNodeCache;
if (mappedNodeCache.isEmpty()) return null;
Set<EventFilter<E, ?>> filters = new HashSet<>(mappedNodeCache.size());
Map<Object, Handle<E>> handlers = new HashMap<>(mappedNodeCache.size());
// Retrieve all filters used to retrieve potential handlers
for (var mappedEntry : mappedNodeCache.entrySet()) {
final EventNodeImpl<E> mappedNode = mappedEntry.getValue();
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);
handlers.put(mappedEntry.getKey(), handle);
}
// If at least one mapped node listen to this handle type,
// loop through them and forward to mapped node if there is a match
if (!filters.isEmpty()) {
final var filterList = List.copyOf(filters);
final int size = filterList.size();
final BiConsumer<EventFilter<E, ?>, E> mapper = (filter, event) -> {
final Object handler = filter.castHandler(event);
final Handle<E> handle = handlers.get(handler);
if (handle != null) { // Run the listeners of the mapped node
if (!handle.updated) handle.update();
for (Consumer<E> listener : handle.listeners) {
listener.accept(event);
}
if (filters.isEmpty()) return null;
final EventFilter<E, ?>[] filterList = filters.toArray(EventFilter[]::new);
final BiConsumer<EventFilter<E, ?>, E> mapper = (filter, event) -> {
final Object handler = filter.castHandler(event);
final Handle<E> handle = handlers.get(handler);
if (handle != null) handle.call(event);
};
if (filterList.length == 1) {
final var firstFilter = filterList[0];
// Common case where there is only one filter
return event -> mapper.accept(firstFilter, event);
} else if (filterList.length == 2) {
final var firstFilter = filterList[0];
final var secondFilter = filterList[1];
return event -> {
mapper.accept(firstFilter, event);
mapper.accept(secondFilter, event);
};
} else {
return event -> {
for (var filter : filterList) {
mapper.accept(filter, event);
}
};
if (size == 1) {
final var firstFilter = filterList.get(0);
// Common case where there is only one filter
this.listeners.add(event -> mapper.accept(firstFilter, event));
} else if (size == 2) {
final var firstFilter = filterList.get(0);
final var secondFilter = filterList.get(1);
this.listeners.add(event -> {
mapper.accept(firstFilter, event);
mapper.accept(secondFilter, event);
});
} else {
this.listeners.add(event -> {
for (var filter : filterList) {
mapper.accept(filter, event);
}
});
}
}
}
/**
* Add listeners from {@link EventNode#addListener(EventListener)} and
* {@link EventNode#register(EventBinding)} to the handle list.
* <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);
}
void callListener(@NotNull EventListener<E> listener, E event) {
EventListener.Result result = listener.run(event);
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;
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.
* <p>
* It is recommended to store instances of this class in {@code static final} fields.
*
* @param <E> the event type
*/
@ApiStatus.Experimental
@ApiStatus.NonExtendable
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
*/
public void handleException(Throwable e) {
// TODO handle OOM exceptions
this.getExceptionHandler().handleException(e);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -401,18 +401,7 @@ public class Inventory extends AbstractInventory implements Viewable {
final InventoryClickResult clickResult = clickProcessor.dragging(player,
slot != -999 ? (isInWindow ? this : playerInventory) : null,
clickSlot, button,
clicked, cursor,
s -> isClickInWindow(s) ? getItemStack(s) :
playerInventory.getItemStack(PlayerInventoryUtils.convertSlot(s, offset)),
(s, item) -> {
if (isClickInWindow(s)) {
setItemStack(s, item);
} else {
playerInventory.setItemStack(PlayerInventoryUtils.convertSlot(s, offset), item);
}
});
clicked, cursor);
if (clickResult == null || clickResult.isCancel()) {
updateAll(player);
return false;

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import net.minestom.server.item.rule.VanillaStackingRule;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.utils.NBTUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@ -79,6 +80,24 @@ public final class ItemStack implements TagReadable, HoverEventSource<HoverEvent
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)
public @NotNull Material getMaterial() {
return material;
@ -228,6 +247,20 @@ public final class ItemStack implements TagReadable, HoverEventSource<HoverEvent
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)
private @NotNull ItemStackBuilder 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_ROCKET, FireworkMeta.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_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;
}
// Place the block
instance.placeBlock(player, resultBlock, placementPosition,
blockFace, packet.cursorPositionX, packet.cursorPositionY, packet.cursorPositionZ);
instance.placeBlock(new BlockHandler.PlayerPlacement(resultBlock, instance, placementPosition, player, hand, blockFace,
packet.cursorPositionX, packet.cursorPositionY, packet.cursorPositionZ));
// Block consuming
if (playerBlockPlaceEvent.doesConsumeBlock()) {
// Consume the block in the player's hand

View File

@ -2,11 +2,12 @@ package net.minestom.server.listener;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.GlobalHandles;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.client.play.*;
import net.minestom.server.network.packet.server.play.PlayerPositionAndLookPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
@ -32,9 +33,9 @@ public class PlayerPositionListener {
player.refreshReceivedTeleportId(packet.teleportId);
}
private static void processMovement(@NotNull Player player, @NotNull Pos newPosition, boolean onGround) {
private static void processMovement(@NotNull Player player, @NotNull Pos packetPosition, boolean onGround) {
final var currentPosition = player.getPosition();
if (currentPosition.equals(newPosition)) {
if (currentPosition.equals(packetPosition)) {
// For some reason, the position is the same
return;
}
@ -48,21 +49,37 @@ public class PlayerPositionListener {
return;
}
// Try to move in an unloaded chunk, prevent it
if (!currentPosition.sameChunk(newPosition) && !ChunkUtils.isLoaded(instance, newPosition)) {
if (!currentPosition.sameChunk(packetPosition) && !ChunkUtils.isLoaded(instance, packetPosition)) {
player.teleport(currentPosition);
return;
}
PlayerMoveEvent playerMoveEvent = new PlayerMoveEvent(player, newPosition);
EventDispatcher.call(playerMoveEvent, GlobalHandles.PLAYER_MOVE);
// True if the event call changed the player position (possibly a teleport)
if (!playerMoveEvent.isCancelled() && currentPosition.equals(player.getPosition())) {
// Move the player
player.refreshPosition(playerMoveEvent.getNewPosition());
PlayerMoveEvent playerMoveEvent = new PlayerMoveEvent(player, packetPosition);
GlobalHandles.PLAYER_MOVE.call(playerMoveEvent);
if (!currentPosition.equals(player.getPosition())) {
// Player has been teleported in the event
return;
}
if (playerMoveEvent.isCancelled()) {
// Teleport to previous position
PlayerConnection connection = player.getPlayerConnection();
connection.sendPacket(new PlayerPositionAndLookPacket(currentPosition, (byte) 0x00, player.getNextTeleportId(), false));
return;
}
final Pos eventPosition = playerMoveEvent.getNewPosition();
if (packetPosition.equals(eventPosition)) {
// Event didn't change the position
player.refreshPosition(eventPosition);
player.refreshOnGround(onGround);
} else {
// Cancelled, teleport to previous position
player.teleport(player.getPosition());
// Position modified by the event
if (packetPosition.samePoint(eventPosition)) {
player.refreshPosition(eventPosition, true);
player.refreshOnGround(onGround);
player.setView(eventPosition.yaw(), eventPosition.pitch());
} else {
player.teleport(eventPosition);
}
}
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

@ -100,12 +100,26 @@ public class Task implements Runnable {
* Sets up the task for correct execution.
*/
public void schedule() {
if(owningExtension != null) {
this.schedulerManager.onScheduleFromExtension(owningExtension, this);
if(this.shutdown) {
Int2ObjectMap<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}
*/
@NotNull
public Task schedule() {
Task task = new Task(
public Task build() {
return new Task(
this.schedulerManager,
this.runnable,
this.shutdown,
@ -152,21 +152,17 @@ public class TaskBuilder {
this.repeat,
this.isTransient,
this.owningExtension);
if (this.shutdown) {
Int2ObjectMap<Task> shutdownTasks = this.schedulerManager.shutdownTasks;
synchronized (shutdownTasks) {
shutdownTasks.put(task.getId(), task);
}
if (owningExtension != null) {
this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, task);
}
} else {
Int2ObjectMap<Task> tasks = this.schedulerManager.tasks;
synchronized (tasks) {
tasks.put(task.getId(), task);
}
task.schedule();
}
}
/**
* Schedules this {@link Task} for execution.
*
* @return the scheduled {@link Task}
*/
@NotNull
public Task schedule() {
Task task = build();
task.schedule();
return task;
}
}

View File

@ -1,14 +1,15 @@
package net.minestom.server.utils;
import java.util.Random;
import org.jetbrains.annotations.ApiStatus;
import java.util.UUID;
import java.util.regex.Pattern;
/**
* An utilities class for {@link UUID}.
*/
@ApiStatus.Internal
public final class UniqueIdUtils {
public static final Pattern UNIQUE_ID_PATTERN = Pattern.compile("\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b");
/**
@ -20,12 +21,4 @@ public final class UniqueIdUtils {
public static boolean isUniqueId(String input) {
return input.matches(UNIQUE_ID_PATTERN.pattern());
}
public static UUID createRandomUUID(Random random) {
long most = random.nextLong() & -61441L | 16384L;
long least = random.nextLong() & 4611686018427387903L | Long.MAX_VALUE;
return new UUID(most, least);
}
}

View File

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

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.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.metadata.BundleMeta;
import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.utils.MathUtils;
@ -104,6 +105,14 @@ public class PlayerInit {
.canDestroy(Set.of(Block.DIAMOND_ORE)))
.build();
player.getInventory().addItemStack(itemStack);
ItemStack bundle = ItemStack.builder(Material.BUNDLE)
.meta(BundleMeta.class, bundleMetaBuilder -> {
bundleMetaBuilder.addItem(ItemStack.of(Material.DIAMOND, 5));
bundleMetaBuilder.addItem(ItemStack.of(Material.RABBIT_FOOT, 5));
})
.build();
player.getInventory().addItemStack(bundle);
});
static {

View File

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