mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-30 13:08:19 +01:00
Merge upstream
This commit is contained in:
commit
9531bd5cb3
@ -7,8 +7,7 @@ import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.InstanceManager;
|
||||
import net.minestom.server.monitoring.TickMonitor;
|
||||
import net.minestom.server.network.ConnectionManager;
|
||||
import net.minestom.server.thread.SingleThreadProvider;
|
||||
import net.minestom.server.thread.ThreadProvider;
|
||||
import net.minestom.server.thread.ThreadDispatcher;
|
||||
import net.minestom.server.utils.PacketUtils;
|
||||
import net.minestom.server.utils.async.AsyncUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -24,14 +23,14 @@ import java.util.function.LongConsumer;
|
||||
/**
|
||||
* Manager responsible for the server ticks.
|
||||
* <p>
|
||||
* The {@link ThreadProvider} manages the multi-thread aspect of chunk ticks.
|
||||
* The {@link ThreadDispatcher} manages the multi-thread aspect of chunk ticks.
|
||||
*/
|
||||
public final class UpdateManager {
|
||||
|
||||
private volatile boolean stopRequested;
|
||||
|
||||
// TODO make configurable
|
||||
private ThreadProvider threadProvider = new SingleThreadProvider();
|
||||
private ThreadDispatcher threadDispatcher = ThreadDispatcher.singleThread();
|
||||
|
||||
private final Queue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>();
|
||||
private final Queue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
|
||||
@ -96,7 +95,7 @@ public final class UpdateManager {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
}
|
||||
this.threadProvider.shutdown();
|
||||
this.threadDispatcher.shutdown();
|
||||
}, MinecraftServer.THREAD_NAME_TICK_SCHEDULER).start();
|
||||
}
|
||||
|
||||
@ -115,11 +114,11 @@ public final class UpdateManager {
|
||||
}
|
||||
});
|
||||
// Tick all chunks (and entities inside)
|
||||
this.threadProvider.updateAndAwait(tickStart);
|
||||
this.threadDispatcher.updateAndAwait(tickStart);
|
||||
|
||||
// Clear removed entities & update threads
|
||||
long tickTime = System.currentTimeMillis() - tickStart;
|
||||
this.threadProvider.refreshThreads(tickTime);
|
||||
final long tickTime = System.currentTimeMillis() - tickStart;
|
||||
this.threadDispatcher.refreshThreads(tickTime);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,56 +137,56 @@ public final class UpdateManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current {@link ThreadProvider}.
|
||||
* Gets the current {@link ThreadDispatcher}.
|
||||
*
|
||||
* @return the current thread provider
|
||||
*/
|
||||
public @NotNull ThreadProvider getThreadProvider() {
|
||||
return threadProvider;
|
||||
public @NotNull ThreadDispatcher getThreadProvider() {
|
||||
return threadDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the {@link ThreadProvider} that an instance has been created.
|
||||
* Signals the {@link ThreadDispatcher} that an instance has been created.
|
||||
* <p>
|
||||
* WARNING: should be automatically done by the {@link InstanceManager}.
|
||||
*
|
||||
* @param instance the instance
|
||||
*/
|
||||
public void signalInstanceCreate(Instance instance) {
|
||||
this.threadProvider.onInstanceCreate(instance);
|
||||
this.threadDispatcher.onInstanceCreate(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the {@link ThreadProvider} that an instance has been deleted.
|
||||
* Signals the {@link ThreadDispatcher} that an instance has been deleted.
|
||||
* <p>
|
||||
* WARNING: should be automatically done by the {@link InstanceManager}.
|
||||
*
|
||||
* @param instance the instance
|
||||
*/
|
||||
public void signalInstanceDelete(Instance instance) {
|
||||
this.threadProvider.onInstanceDelete(instance);
|
||||
this.threadDispatcher.onInstanceDelete(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the {@link ThreadProvider} that a chunk has been loaded.
|
||||
* Signals the {@link ThreadDispatcher} that a chunk has been loaded.
|
||||
* <p>
|
||||
* WARNING: should be automatically done by the {@link Instance} implementation.
|
||||
*
|
||||
* @param chunk the loaded chunk
|
||||
*/
|
||||
public void signalChunkLoad(@NotNull Chunk chunk) {
|
||||
this.threadProvider.onChunkLoad(chunk);
|
||||
this.threadDispatcher.onChunkLoad(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the {@link ThreadProvider} that a chunk has been unloaded.
|
||||
* Signals the {@link ThreadDispatcher} that a chunk has been unloaded.
|
||||
* <p>
|
||||
* WARNING: should be automatically done by the {@link Instance} implementation.
|
||||
*
|
||||
* @param chunk the unloaded chunk
|
||||
*/
|
||||
public void signalChunkUnload(@NotNull Chunk chunk) {
|
||||
this.threadProvider.onChunkUnload(chunk);
|
||||
this.threadDispatcher.onChunkUnload(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,13 @@
|
||||
package net.minestom.server.acquirable;
|
||||
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.thread.ThreadProvider;
|
||||
import net.minestom.server.thread.ThreadDispatcher;
|
||||
import net.minestom.server.thread.TickThread;
|
||||
import net.minestom.server.utils.async.AsyncUtils;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
@ -37,7 +36,7 @@ public interface Acquirable<T> {
|
||||
* @param entries the new chunk entries
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
static void refreshEntries(@NotNull Collection<ThreadProvider.ChunkEntry> entries) {
|
||||
static void refreshEntries(@NotNull Collection<ThreadDispatcher.ChunkEntry> entries) {
|
||||
AcquirableImpl.ENTRIES.set(entries);
|
||||
}
|
||||
|
||||
@ -85,8 +84,7 @@ public interface Acquirable<T> {
|
||||
* @see #sync(Consumer) for auto-closeable capability
|
||||
*/
|
||||
default @NotNull Acquired<T> lock() {
|
||||
var optional = local();
|
||||
return optional.map(Acquired::local).orElseGet(() -> Acquired.locked(this));
|
||||
return new Acquired<>(unwrap(), getHandler().getTickThread());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,14 +98,19 @@ public interface Acquirable<T> {
|
||||
* {@link Optional#empty()} otherwise
|
||||
*/
|
||||
default @NotNull Optional<T> local() {
|
||||
final Thread currentThread = Thread.currentThread();
|
||||
final TickThread tickThread = getHandler().getTickThread();
|
||||
if (Objects.equals(currentThread, tickThread)) {
|
||||
return Optional.of(unwrap());
|
||||
}
|
||||
if (isLocal()) return Optional.of(unwrap());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if the acquirable element is local to this thread
|
||||
*
|
||||
* @return true if the element is linked to the current thread
|
||||
*/
|
||||
default boolean isLocal() {
|
||||
return Thread.currentThread() == getHandler().getTickThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the acquirable element, execute {@code consumer} synchronously and unlock the thread.
|
||||
* <p>
|
||||
@ -117,7 +120,7 @@ public interface Acquirable<T> {
|
||||
* @see #async(Consumer)
|
||||
*/
|
||||
default void sync(@NotNull Consumer<T> consumer) {
|
||||
var acquired = lock();
|
||||
Acquired<T> acquired = lock();
|
||||
consumer.accept(acquired.get());
|
||||
acquired.unlock();
|
||||
}
|
||||
@ -153,22 +156,21 @@ public interface Acquirable<T> {
|
||||
@ApiStatus.Internal
|
||||
@NotNull Handler getHandler();
|
||||
|
||||
class Handler {
|
||||
final class Handler {
|
||||
private volatile ThreadDispatcher.ChunkEntry chunkEntry;
|
||||
|
||||
private volatile ThreadProvider.ChunkEntry chunkEntry;
|
||||
|
||||
public ThreadProvider.ChunkEntry getChunkEntry() {
|
||||
public ThreadDispatcher.ChunkEntry getChunkEntry() {
|
||||
return chunkEntry;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void refreshChunkEntry(@NotNull ThreadProvider.ChunkEntry chunkEntry) {
|
||||
public void refreshChunkEntry(@NotNull ThreadDispatcher.ChunkEntry chunkEntry) {
|
||||
this.chunkEntry = chunkEntry;
|
||||
}
|
||||
|
||||
public TickThread getTickThread() {
|
||||
return chunkEntry != null ? chunkEntry.getThread() : null;
|
||||
final ThreadDispatcher.ChunkEntry entry = this.chunkEntry;
|
||||
return entry != null ? entry.getThread() : null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.minestom.server.acquirable;
|
||||
|
||||
import net.minestom.server.thread.ThreadProvider;
|
||||
import net.minestom.server.thread.ThreadDispatcher;
|
||||
import net.minestom.server.thread.TickThread;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -10,10 +10,9 @@ import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
class AcquirableImpl<T> implements Acquirable<T> {
|
||||
|
||||
protected static final ThreadLocal<Collection<ThreadProvider.ChunkEntry>> ENTRIES = ThreadLocal.withInitial(Collections::emptySet);
|
||||
protected static final AtomicLong WAIT_COUNTER_NANO = new AtomicLong();
|
||||
final class AcquirableImpl<T> implements Acquirable<T> {
|
||||
static final ThreadLocal<Collection<ThreadDispatcher.ChunkEntry>> ENTRIES = ThreadLocal.withInitial(Collections::emptySet);
|
||||
static final AtomicLong WAIT_COUNTER_NANO = new AtomicLong();
|
||||
|
||||
/**
|
||||
* Global lock used for synchronization.
|
||||
@ -38,41 +37,37 @@ class AcquirableImpl<T> implements Acquirable<T> {
|
||||
return handler;
|
||||
}
|
||||
|
||||
protected static @Nullable ReentrantLock enter(@Nullable Thread currentThread, @Nullable TickThread elementThread) {
|
||||
// Monitoring
|
||||
long time = System.nanoTime();
|
||||
|
||||
ReentrantLock currentLock;
|
||||
{
|
||||
final TickThread current = currentThread instanceof TickThread ?
|
||||
(TickThread) currentThread : null;
|
||||
currentLock = current != null && current.getLock().isHeldByCurrentThread() ?
|
||||
current.getLock() : null;
|
||||
}
|
||||
if (currentLock != null)
|
||||
currentLock.unlock();
|
||||
|
||||
GLOBAL_LOCK.lock();
|
||||
|
||||
if (currentLock != null)
|
||||
currentLock.lock();
|
||||
|
||||
final var lock = elementThread != null ? elementThread.getLock() : null;
|
||||
final boolean acquired = lock == null || lock.isHeldByCurrentThread();
|
||||
if (!acquired) {
|
||||
lock.lock();
|
||||
}
|
||||
static @Nullable ReentrantLock enter(@NotNull Thread currentThread, @Nullable TickThread elementThread) {
|
||||
if (elementThread == null) return null;
|
||||
if (currentThread == elementThread) return null;
|
||||
final ReentrantLock currentLock = currentThread instanceof TickThread ? ((TickThread) currentThread).getLock() : null;
|
||||
final ReentrantLock targetLock = elementThread.getLock();
|
||||
if (targetLock.isHeldByCurrentThread()) return null;
|
||||
|
||||
// Monitoring
|
||||
AcquirableImpl.WAIT_COUNTER_NANO.addAndGet(System.nanoTime() - time);
|
||||
final long time = System.nanoTime();
|
||||
|
||||
return !acquired ? lock : null;
|
||||
// Enter the target thread
|
||||
// TODO reduce global lock scope
|
||||
if (currentLock != null) {
|
||||
while (!GLOBAL_LOCK.tryLock()) {
|
||||
currentLock.unlock();
|
||||
currentLock.lock();
|
||||
}
|
||||
} else {
|
||||
GLOBAL_LOCK.lock();
|
||||
}
|
||||
targetLock.lock();
|
||||
|
||||
// Monitoring
|
||||
WAIT_COUNTER_NANO.addAndGet(System.nanoTime() - time);
|
||||
return targetLock;
|
||||
}
|
||||
|
||||
protected static void leave(@Nullable ReentrantLock lock) {
|
||||
static void leave(@Nullable ReentrantLock lock) {
|
||||
if (lock != null) {
|
||||
lock.unlock();
|
||||
GLOBAL_LOCK.unlock();
|
||||
}
|
||||
GLOBAL_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
@ -6,47 +6,39 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class Acquired<T> {
|
||||
|
||||
/**
|
||||
* Represents an object that has been safely acquired and can be freed again.
|
||||
* <p>
|
||||
* This class should not be shared, and it is recommended to call {@link #unlock()}
|
||||
* once the acquisition goal has been fulfilled to limit blocking time.
|
||||
*
|
||||
* @param <T> the type of the acquired object
|
||||
*/
|
||||
public final class Acquired<T> {
|
||||
private final T value;
|
||||
|
||||
private final boolean locked;
|
||||
private final Thread owner;
|
||||
private final ReentrantLock lock;
|
||||
|
||||
private boolean unlocked;
|
||||
|
||||
protected static <T> Acquired<T> local(@NotNull T value) {
|
||||
return new Acquired<>(value, false, null, null);
|
||||
}
|
||||
|
||||
protected static <T> Acquired<T> locked(@NotNull Acquirable<T> acquirable) {
|
||||
final Thread currentThread = Thread.currentThread();
|
||||
final TickThread tickThread = acquirable.getHandler().getTickThread();
|
||||
return new Acquired<>(acquirable.unwrap(), true, currentThread, tickThread);
|
||||
}
|
||||
|
||||
private Acquired(@NotNull T value,
|
||||
boolean locked, Thread currentThread, TickThread tickThread) {
|
||||
Acquired(T value, TickThread tickThread) {
|
||||
this.value = value;
|
||||
this.locked = locked;
|
||||
this.lock = locked ? AcquirableImpl.enter(currentThread, tickThread) : null;
|
||||
this.owner = Thread.currentThread();
|
||||
this.lock = AcquirableImpl.enter(owner, tickThread);
|
||||
}
|
||||
|
||||
public @NotNull T get() {
|
||||
checkLock();
|
||||
safeCheck();
|
||||
return value;
|
||||
}
|
||||
|
||||
public void unlock() {
|
||||
checkLock();
|
||||
safeCheck();
|
||||
this.unlocked = true;
|
||||
if (!locked)
|
||||
return;
|
||||
AcquirableImpl.leave(lock);
|
||||
}
|
||||
|
||||
private void checkLock() {
|
||||
private void safeCheck() {
|
||||
Check.stateCondition(Thread.currentThread() != owner, "Acquired object is owned by the thread {0}", owner);
|
||||
Check.stateCondition(unlocked, "The acquired element has already been unlocked!");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import net.minestom.server.potion.PotionEffect;
|
||||
import net.minestom.server.potion.TimedPotion;
|
||||
import net.minestom.server.tag.Tag;
|
||||
import net.minestom.server.tag.TagHandler;
|
||||
import net.minestom.server.thread.ThreadProvider;
|
||||
import net.minestom.server.utils.PacketUtils;
|
||||
import net.minestom.server.utils.async.AsyncUtils;
|
||||
import net.minestom.server.utils.block.BlockIterator;
|
||||
@ -164,7 +163,6 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
|
||||
/**
|
||||
* Schedules a task to be run during the next entity tick.
|
||||
* It ensures that the task will be executed in the same thread as the entity (depending of the {@link ThreadProvider}).
|
||||
*
|
||||
* @param callback the task to execute during the next entity tick
|
||||
*/
|
||||
@ -438,7 +436,6 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
// Entity tick
|
||||
{
|
||||
// Cache the number of "gravity tick"
|
||||
this.gravityTickCount = onGround ? 0 : gravityTickCount + 1;
|
||||
velocityTick();
|
||||
|
||||
// handle block contacts
|
||||
@ -465,6 +462,8 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
}
|
||||
|
||||
private void velocityTick() {
|
||||
this.gravityTickCount = onGround ? 0 : gravityTickCount + 1;
|
||||
|
||||
final boolean isSocketClient = PlayerUtils.isSocketClient(this);
|
||||
if (isSocketClient) {
|
||||
if (position.samePoint(previousPosition))
|
||||
@ -626,8 +625,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return the entity type
|
||||
*/
|
||||
@NotNull
|
||||
public EntityType getEntityType() {
|
||||
public @NotNull EntityType getEntityType() {
|
||||
return entityType;
|
||||
}
|
||||
|
||||
@ -636,8 +634,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return the entity unique id
|
||||
*/
|
||||
@NotNull
|
||||
public UUID getUuid() {
|
||||
public @NotNull UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@ -650,7 +647,6 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
// Refresh internal map
|
||||
Entity.ENTITY_BY_UUID.remove(this.uuid);
|
||||
Entity.ENTITY_BY_UUID.put(uuid, this);
|
||||
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@ -668,8 +664,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return the entity bounding box
|
||||
*/
|
||||
@NotNull
|
||||
public BoundingBox getBoundingBox() {
|
||||
public @NotNull BoundingBox getBoundingBox() {
|
||||
return boundingBox;
|
||||
}
|
||||
|
||||
@ -868,8 +863,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return the entity vehicle, or null if there is not any
|
||||
*/
|
||||
@Nullable
|
||||
public Entity getVehicle() {
|
||||
public @Nullable Entity getVehicle() {
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
@ -927,13 +921,11 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return an unmodifiable list containing all the entity passengers
|
||||
*/
|
||||
@NotNull
|
||||
public Set<Entity> getPassengers() {
|
||||
public @NotNull Set<@NotNull Entity> getPassengers() {
|
||||
return Collections.unmodifiableSet(passengers);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected SetPassengersPacket getPassengersPacket() {
|
||||
protected @NotNull SetPassengersPacket getPassengersPacket() {
|
||||
SetPassengersPacket passengersPacket = new SetPassengersPacket();
|
||||
passengersPacket.vehicleEntityId = getEntityId();
|
||||
|
||||
@ -1064,8 +1056,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return the entity pose
|
||||
*/
|
||||
@NotNull
|
||||
public Pose getPose() {
|
||||
public @NotNull Pose getPose() {
|
||||
return this.entityMeta.getPose();
|
||||
}
|
||||
|
||||
@ -1253,8 +1244,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
|
||||
*
|
||||
* @return an unmodifiable list of all this entity effects
|
||||
*/
|
||||
@NotNull
|
||||
public List<TimedPotion> getActiveEffects() {
|
||||
public @NotNull List<@NotNull TimedPotion> getActiveEffects() {
|
||||
return Collections.unmodifiableList(effects);
|
||||
}
|
||||
|
||||
|
@ -1177,13 +1177,16 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
* @param newChunk the current/new player chunk (can be the current one)
|
||||
*/
|
||||
public void refreshVisibleChunks(@NotNull Chunk newChunk) {
|
||||
final int newChunkX = newChunk.getChunkX();
|
||||
final int newChunkZ = newChunk.getChunkZ();
|
||||
final int range = getChunkRange();
|
||||
// Previous chunks indexes
|
||||
final long[] lastVisibleChunks = viewableChunks.stream().mapToLong(ChunkUtils::getChunkIndex).toArray();
|
||||
// New chunks indexes
|
||||
final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunk.toPosition(), getChunkRange());
|
||||
final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunkX, newChunkZ, range);
|
||||
|
||||
// Update client render distance
|
||||
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
|
||||
updateViewPosition(newChunkX, newChunkZ);
|
||||
|
||||
// Unload old chunks
|
||||
ArrayUtils.forDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks, chunkIndex -> {
|
||||
@ -1259,11 +1262,20 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*
|
||||
* @return the player connection
|
||||
*/
|
||||
@NotNull
|
||||
public PlayerConnection getPlayerConnection() {
|
||||
public @NotNull PlayerConnection getPlayerConnection() {
|
||||
return playerConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for {@link PlayerConnection#sendPacket(ServerPacket)}.
|
||||
*
|
||||
* @param packet the packet to send
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
public void sendPacket(@NotNull ServerPacket packet) {
|
||||
this.playerConnection.sendPacket(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if the player is online or not.
|
||||
*
|
||||
@ -1278,8 +1290,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*
|
||||
* @return the player settings
|
||||
*/
|
||||
@NotNull
|
||||
public PlayerSettings getSettings() {
|
||||
public @NotNull PlayerSettings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@ -1292,8 +1303,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
return dimensionType;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PlayerInventory getInventory() {
|
||||
public @NotNull PlayerInventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
|
||||
@ -1440,8 +1450,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*
|
||||
* @return the currently open inventory, null if there is not (player inventory is not detected)
|
||||
*/
|
||||
@Nullable
|
||||
public Inventory getOpenInventory() {
|
||||
public @Nullable Inventory getOpenInventory() {
|
||||
return openInventory;
|
||||
}
|
||||
|
||||
@ -1452,7 +1461,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
* @return true if the inventory has been opened/sent to the player, false otherwise (cancelled by event)
|
||||
*/
|
||||
public boolean openInventory(@NotNull Inventory inventory) {
|
||||
|
||||
InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this);
|
||||
|
||||
EventDispatcher.callCancellable(inventoryOpenEvent, () -> {
|
||||
@ -1462,7 +1470,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
}
|
||||
|
||||
Inventory newInventory = inventoryOpenEvent.getInventory();
|
||||
|
||||
if (newInventory == null) {
|
||||
// just close the inventory
|
||||
return;
|
||||
@ -1474,9 +1481,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
playerConnection.sendPacket(openWindowPacket);
|
||||
newInventory.addViewer(this);
|
||||
this.openInventory = newInventory;
|
||||
|
||||
});
|
||||
|
||||
return !inventoryOpenEvent.isCancelled();
|
||||
}
|
||||
|
||||
@ -1935,8 +1940,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*
|
||||
* @return a {@link PlayerInfoPacket} to add the player
|
||||
*/
|
||||
@NotNull
|
||||
protected PlayerInfoPacket getAddPlayerToList() {
|
||||
protected @NotNull PlayerInfoPacket getAddPlayerToList() {
|
||||
PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.ADD_PLAYER);
|
||||
|
||||
PlayerInfoPacket.AddPlayer addPlayer =
|
||||
@ -1962,14 +1966,9 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*
|
||||
* @return a {@link PlayerInfoPacket} to remove the player
|
||||
*/
|
||||
@NotNull
|
||||
protected PlayerInfoPacket getRemovePlayerToList() {
|
||||
protected @NotNull PlayerInfoPacket getRemovePlayerToList() {
|
||||
PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER);
|
||||
|
||||
PlayerInfoPacket.RemovePlayer removePlayer =
|
||||
new PlayerInfoPacket.RemovePlayer(getUuid());
|
||||
|
||||
playerInfoPacket.playerInfos.add(removePlayer);
|
||||
playerInfoPacket.playerInfos.add(new PlayerInfoPacket.RemovePlayer(getUuid()));
|
||||
return playerInfoPacket;
|
||||
}
|
||||
|
||||
@ -1997,9 +1996,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
connection.sendPacket(new EntityHeadLookPacket(getEntityId(), position.yaw()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemInMainHand() {
|
||||
public @NotNull ItemStack getItemInMainHand() {
|
||||
return inventory.getItemInMainHand();
|
||||
}
|
||||
|
||||
@ -2008,9 +2006,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
inventory.setItemInMainHand(itemStack);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemInOffHand() {
|
||||
public @NotNull ItemStack getItemInOffHand() {
|
||||
return inventory.getItemInOffHand();
|
||||
}
|
||||
|
||||
@ -2019,9 +2016,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
inventory.setItemInOffHand(itemStack);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getHelmet() {
|
||||
public @NotNull ItemStack getHelmet() {
|
||||
return inventory.getHelmet();
|
||||
}
|
||||
|
||||
@ -2030,9 +2026,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
inventory.setHelmet(itemStack);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getChestplate() {
|
||||
public @NotNull ItemStack getChestplate() {
|
||||
return inventory.getChestplate();
|
||||
}
|
||||
|
||||
@ -2041,9 +2036,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
inventory.setChestplate(itemStack);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getLeggings() {
|
||||
public @NotNull ItemStack getLeggings() {
|
||||
return inventory.getLeggings();
|
||||
}
|
||||
|
||||
@ -2052,9 +2046,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
inventory.setLeggings(itemStack);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getBoots() {
|
||||
public @NotNull ItemStack getBoots() {
|
||||
return inventory.getBoots();
|
||||
}
|
||||
|
||||
@ -2131,16 +2124,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated See {@link ChatMessageType}
|
||||
*/
|
||||
@Deprecated
|
||||
public enum ChatMode {
|
||||
ENABLED,
|
||||
COMMANDS_ONLY,
|
||||
HIDDEN
|
||||
}
|
||||
|
||||
public class PlayerSettings {
|
||||
|
||||
private String locale;
|
||||
@ -2172,17 +2155,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
return viewDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player chat mode.
|
||||
*
|
||||
* @return the player chat mode
|
||||
* @deprecated Use {@link #getChatMessageType()}
|
||||
*/
|
||||
@Deprecated
|
||||
public ChatMode getChatMode() {
|
||||
return ChatMode.values()[chatMessageType.ordinal()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the messages this player wants to receive.
|
||||
*
|
||||
|
@ -139,8 +139,12 @@ public class AnvilLoader implements IChunkLoader {
|
||||
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
|
||||
try {
|
||||
final BlockState blockState = section.get(x, y, z);
|
||||
final Block block = Objects.requireNonNull(Block.fromNamespaceId(blockState.getName()))
|
||||
Block block = Objects.requireNonNull(Block.fromNamespaceId(blockState.getName()))
|
||||
.withProperties(blockState.getProperties());
|
||||
BlockHandler handler = MinecraftServer.getBlockManager().getHandler(block.name());
|
||||
if (handler != null) {
|
||||
block = block.withHandler(handler);
|
||||
}
|
||||
chunk.setBlock(x, y + yOffset, z, block);
|
||||
} catch (Exception e) {
|
||||
EXCEPTION_MANAGER.handleException(e);
|
||||
|
@ -200,7 +200,7 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
|
||||
* @return the position of this chunk
|
||||
*/
|
||||
public @NotNull Point toPosition() {
|
||||
return new Vec(CHUNK_SIZE_Z * getChunkX(), 0, CHUNK_SIZE_Z * getChunkZ());
|
||||
return new Vec(CHUNK_SIZE_X * getChunkX(), 0, CHUNK_SIZE_Z * getChunkZ());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import com.extollit.gaming.ai.path.model.ColumnarOcclusionFieldList;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.pathfinding.PFBlock;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
@ -93,10 +93,7 @@ public class DynamicChunk extends Chunk {
|
||||
final Block block = entry.getValue();
|
||||
final BlockHandler handler = block.handler();
|
||||
if (handler == null) return;
|
||||
final int x = ChunkUtils.blockIndexToChunkPositionX(index);
|
||||
final int y = ChunkUtils.blockIndexToChunkPositionY(index);
|
||||
final int z = ChunkUtils.blockIndexToChunkPositionZ(index);
|
||||
final Vec blockPosition = new Vec(x, y, z);
|
||||
final Point blockPosition = ChunkUtils.getBlockPosition(index, chunkX, chunkZ);
|
||||
handler.tick(new BlockHandler.Tick(block, instance, blockPosition));
|
||||
});
|
||||
}
|
||||
@ -104,18 +101,17 @@ public class DynamicChunk extends Chunk {
|
||||
@Override
|
||||
public @Nullable Block getBlock(int x, int y, int z, @NotNull Condition condition) {
|
||||
// Verify if the block object is present
|
||||
final var entry = !entries.isEmpty() ?
|
||||
final Block entry = !entries.isEmpty() ?
|
||||
entries.get(ChunkUtils.getBlockIndex(x, y, z)) : null;
|
||||
if (entry != null || condition == Condition.CACHED) {
|
||||
return entry;
|
||||
}
|
||||
// Retrieve the block from state id
|
||||
final Section section = getOptionalSection(y);
|
||||
if (section == null)
|
||||
return Block.AIR;
|
||||
if (section == null) return Block.AIR; // Section is unloaded
|
||||
final short blockStateId = section.getBlockAt(x, y, z);
|
||||
return blockStateId > 0 ?
|
||||
Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR) : Block.AIR;
|
||||
if (blockStateId == -1) return Block.AIR; // Section is empty
|
||||
return Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -26,7 +26,6 @@ import net.minestom.server.network.packet.server.play.BlockActionPacket;
|
||||
import net.minestom.server.network.packet.server.play.TimeUpdatePacket;
|
||||
import net.minestom.server.tag.Tag;
|
||||
import net.minestom.server.tag.TagHandler;
|
||||
import net.minestom.server.thread.ThreadProvider;
|
||||
import net.minestom.server.utils.PacketUtils;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
import net.minestom.server.utils.entity.EntityUtils;
|
||||
@ -129,8 +128,6 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta
|
||||
|
||||
/**
|
||||
* Schedules a task to be run during the next instance tick.
|
||||
* It ensures that the task will be executed in the same thread as the instance
|
||||
* and its chunks/entities (depending of the {@link ThreadProvider}).
|
||||
*
|
||||
* @param callback the task to execute during the next instance tick
|
||||
*/
|
||||
|
@ -295,23 +295,23 @@ public interface BlockHandler {
|
||||
* Handler used for loaded blocks with unknown namespace
|
||||
* in order to do not lose the information while saving, and for runtime debugging purpose.
|
||||
*/
|
||||
class Dummy implements BlockHandler {
|
||||
@ApiStatus.Internal
|
||||
final class Dummy implements BlockHandler {
|
||||
private static final Map<String, BlockHandler> DUMMY_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static @NotNull BlockHandler get(@NotNull String namespace) {
|
||||
return DUMMY_CACHE.computeIfAbsent(namespace, s -> new Dummy(NamespaceID.from(namespace)));
|
||||
return DUMMY_CACHE.computeIfAbsent(namespace, Dummy::new);
|
||||
}
|
||||
|
||||
private final NamespaceID namespaceID;
|
||||
private final NamespaceID namespace;
|
||||
|
||||
private Dummy(NamespaceID namespaceID) {
|
||||
this.namespaceID = namespaceID;
|
||||
private Dummy(String name) {
|
||||
namespace = NamespaceID.from(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull NamespaceID getNamespaceId() {
|
||||
return namespaceID;
|
||||
return namespace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.minestom.server.instance.block;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.minestom.server.instance.block.rule.BlockPlacementRule;
|
||||
import net.minestom.server.utils.NamespaceID;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -23,17 +24,21 @@ public class BlockManager {
|
||||
// block id -> block placement rule
|
||||
private final Int2ObjectMap<BlockPlacementRule> placementRuleMap = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
public synchronized void registerHandler(@NotNull String namespace, @NotNull Supplier<@NotNull BlockHandler> handlerSupplier) {
|
||||
this.blockHandlerMap.put(namespace, handlerSupplier);
|
||||
public void registerHandler(@NotNull String namespace, @NotNull Supplier<@NotNull BlockHandler> handlerSupplier) {
|
||||
blockHandlerMap.put(namespace, handlerSupplier);
|
||||
}
|
||||
|
||||
public synchronized @Nullable BlockHandler getHandler(@NotNull String namespace) {
|
||||
public void registerHandler(@NotNull NamespaceID namespace, @NotNull Supplier<@NotNull BlockHandler> handlerSupplier) {
|
||||
registerHandler(namespace.toString(), handlerSupplier);
|
||||
}
|
||||
|
||||
public @Nullable BlockHandler getHandler(@NotNull String namespace) {
|
||||
final var handler = blockHandlerMap.get(namespace);
|
||||
return handler != null ? handler.get() : null;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public synchronized @Nullable BlockHandler getHandlerOrDummy(@NotNull String namespace) {
|
||||
public @NotNull BlockHandler getHandlerOrDummy(@NotNull String namespace) {
|
||||
BlockHandler handler = getHandler(namespace);
|
||||
if (handler == null) {
|
||||
LOGGER.warn("Block {} does not have any corresponding handler, default to dummy.", namespace);
|
||||
@ -51,7 +56,7 @@ public class BlockManager {
|
||||
public synchronized void registerBlockPlacementRule(@NotNull BlockPlacementRule blockPlacementRule) {
|
||||
final int id = blockPlacementRule.getBlock().id();
|
||||
Check.argCondition(id < 0, "Block ID must be >= 0, got: " + id);
|
||||
this.placementRuleMap.put(id, blockPlacementRule);
|
||||
placementRuleMap.put(id, blockPlacementRule);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,7 +317,7 @@ public class Inventory extends AbstractInventory implements Viewable {
|
||||
isInWindow ? this : playerInventory,
|
||||
isInWindow ? playerInventory : this,
|
||||
0, isInWindow ? playerInventory.getInnerSize() : getInnerSize(), 1,
|
||||
player, slot, clicked, cursor);
|
||||
player, clickSlot, clicked, cursor);
|
||||
if (clickResult.isCancel()) {
|
||||
updateAll(player);
|
||||
return false;
|
||||
@ -421,7 +421,7 @@ public class Inventory extends AbstractInventory implements Viewable {
|
||||
ItemStack.AIR;
|
||||
final ItemStack cursor = getCursorItem(player);
|
||||
final InventoryClickResult clickResult = clickProcessor.doubleClick(isInWindow ? this : playerInventory,
|
||||
this, player, slot, clicked, cursor);
|
||||
this, player, clickSlot, clicked, cursor);
|
||||
if (clickResult.isCancel()) {
|
||||
updateAll(player);
|
||||
return false;
|
||||
|
@ -8,7 +8,6 @@ import net.minestom.server.event.inventory.PlayerInventoryItemChangeEvent;
|
||||
import net.minestom.server.event.item.EntityEquipEvent;
|
||||
import net.minestom.server.inventory.click.ClickType;
|
||||
import net.minestom.server.inventory.click.InventoryClickResult;
|
||||
import net.minestom.server.inventory.condition.InventoryCondition;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.network.packet.server.play.SetSlotPacket;
|
||||
import net.minestom.server.network.packet.server.play.WindowItemsPacket;
|
||||
@ -31,17 +30,6 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInventoryCondition(@NotNull InventoryCondition inventoryCondition) {
|
||||
// fix packet slot to inventory slot conversion
|
||||
InventoryCondition condition = (p, slot, clickType, inventoryConditionResult) -> {
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
inventoryCondition.accept(p, convertedSlot, clickType, inventoryConditionResult);
|
||||
};
|
||||
|
||||
super.addInventoryCondition(condition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
super.clear();
|
||||
@ -250,18 +238,19 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
|
||||
|
||||
@Override
|
||||
public boolean drop(@NotNull Player player, boolean all, int slot, int button) {
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
final ItemStack cursor = getCursorItem();
|
||||
final boolean outsideDrop = slot == -999;
|
||||
final ItemStack clicked = outsideDrop ? ItemStack.AIR : getItemStackFromPacketSlot(slot);
|
||||
final ItemStack clicked = outsideDrop ? ItemStack.AIR : getItemStack(convertedSlot);
|
||||
final InventoryClickResult clickResult = clickProcessor.drop(player, this,
|
||||
all, slot, button, clicked, cursor);
|
||||
all, convertedSlot, button, clicked, cursor);
|
||||
if (clickResult.isCancel()) {
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
final ItemStack resultClicked = clickResult.getClicked();
|
||||
if (resultClicked != null && !outsideDrop) {
|
||||
setItemStackFromPacketSlot(slot, resultClicked);
|
||||
setItemStack(convertedSlot, resultClicked);
|
||||
}
|
||||
setCursorItem(clickResult.getCursor());
|
||||
return true;
|
||||
@ -269,20 +258,21 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
|
||||
|
||||
@Override
|
||||
public boolean shiftClick(@NotNull Player player, int slot) {
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
final ItemStack cursor = getCursorItem();
|
||||
final ItemStack clicked = getItemStackFromPacketSlot(slot);
|
||||
final ItemStack clicked = getItemStack(convertedSlot);
|
||||
final boolean hotBarClick = convertSlot(slot, OFFSET) < 9;
|
||||
final int start = hotBarClick ? 9 : 0;
|
||||
final int end = hotBarClick ? getSize() - 9 : 8;
|
||||
final InventoryClickResult clickResult = clickProcessor.shiftClick(
|
||||
this, this,
|
||||
start, end, 1,
|
||||
player, slot, clicked, cursor);
|
||||
player, convertedSlot, clicked, cursor);
|
||||
if (clickResult.isCancel()) {
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
setItemStackFromPacketSlot(slot, clickResult.getClicked());
|
||||
setItemStack(convertedSlot, clickResult.getClicked());
|
||||
setCursorItem(clickResult.getCursor());
|
||||
update(); // FIXME: currently not properly client-predicted
|
||||
return true;
|
||||
@ -292,16 +282,17 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
|
||||
public boolean changeHeld(@NotNull Player player, int slot, int key) {
|
||||
final ItemStack cursorItem = getCursorItem();
|
||||
if (!cursorItem.isAir()) return false;
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
final ItemStack heldItem = getItemStack(key);
|
||||
final ItemStack clicked = getItemStackFromPacketSlot(slot);
|
||||
final InventoryClickResult clickResult = clickProcessor.changeHeld(player, this, slot, key, clicked, heldItem);
|
||||
final ItemStack clicked = getItemStack(convertedSlot);
|
||||
final InventoryClickResult clickResult = clickProcessor.changeHeld(player, this, convertedSlot, key, clicked, heldItem);
|
||||
if (clickResult.isCancel()) {
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
setItemStackFromPacketSlot(slot, clickResult.getClicked());
|
||||
setItemStack(convertedSlot, clickResult.getClicked());
|
||||
setItemStack(key, clickResult.getCursor());
|
||||
callClickEvent(player, null, slot, ClickType.CHANGE_HELD, clicked, cursorItem);
|
||||
callClickEvent(player, null, convertedSlot, ClickType.CHANGE_HELD, clicked, cursorItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -322,9 +313,10 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
|
||||
|
||||
@Override
|
||||
public boolean doubleClick(@NotNull Player player, int slot) {
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
final ItemStack cursor = getCursorItem();
|
||||
final ItemStack clicked = getItemStackFromPacketSlot(slot);
|
||||
final InventoryClickResult clickResult = clickProcessor.doubleClick(this, this, player, slot, clicked, cursor);
|
||||
final ItemStack clicked = getItemStack(convertedSlot);
|
||||
final InventoryClickResult clickResult = clickProcessor.doubleClick(this, this, player, convertedSlot, clicked, cursor);
|
||||
if (clickResult.isCancel()) {
|
||||
update();
|
||||
return false;
|
||||
|
@ -5,7 +5,7 @@ import net.minestom.server.item.StackingRule;
|
||||
import net.minestom.server.utils.MathUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class VanillaStackingRule implements StackingRule {
|
||||
public final class VanillaStackingRule implements StackingRule {
|
||||
|
||||
@Override
|
||||
public boolean canBeStacked(@NotNull ItemStack item1, @NotNull ItemStack item2) {
|
||||
@ -19,8 +19,7 @@ public class VanillaStackingRule implements StackingRule {
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack apply(@NotNull ItemStack item, int amount) {
|
||||
if (amount > 0)
|
||||
return item.withAmount(amount);
|
||||
if (amount > 0) return item.withAmount(amount);
|
||||
return ItemStack.AIR;
|
||||
}
|
||||
|
||||
@ -37,7 +36,6 @@ public class VanillaStackingRule implements StackingRule {
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
|
||||
return obj != null && getClass() == obj.getClass();
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
/**
|
||||
* Represents a socket connection.
|
||||
@ -88,7 +89,7 @@ public class PlayerSocketConnection extends PlayerConnection {
|
||||
}
|
||||
|
||||
public void processPackets(Worker.Context workerContext, PacketProcessor packetProcessor) {
|
||||
final var readBuffer = workerContext.readBuffer;
|
||||
final BinaryBuffer readBuffer = workerContext.readBuffer;
|
||||
// Decrypt data
|
||||
if (encrypted) {
|
||||
final Cipher cipher = decryptCipher;
|
||||
@ -104,7 +105,6 @@ public class PlayerSocketConnection extends PlayerConnection {
|
||||
readBuffer.clear();
|
||||
readBuffer.writeBytes(output);
|
||||
}
|
||||
final int limit = readBuffer.writerOffset();
|
||||
// Read all packets
|
||||
while (readBuffer.readableBytes() > 0) {
|
||||
final var beginMark = readBuffer.mark();
|
||||
@ -112,42 +112,35 @@ public class PlayerSocketConnection extends PlayerConnection {
|
||||
// Ensure that the buffer contains the full packet (or wait for next socket read)
|
||||
final int packetLength = readBuffer.readVarInt();
|
||||
final int readerStart = readBuffer.readerOffset();
|
||||
final int packetEnd = readerStart + packetLength;
|
||||
if (packetEnd > readBuffer.writerOffset()) {
|
||||
if (!readBuffer.canRead(packetLength)) {
|
||||
// Integrity fail
|
||||
throw new BufferUnderflowException();
|
||||
}
|
||||
// Read packet https://wiki.vg/Protocol#Packet_format
|
||||
BinaryBuffer content;
|
||||
int payloadLength;
|
||||
if (!compressed) {
|
||||
// Compression disabled, payload is following
|
||||
content = readBuffer;
|
||||
payloadLength = packetLength;
|
||||
} else {
|
||||
BinaryBuffer content = readBuffer;
|
||||
int decompressedSize = packetLength;
|
||||
if (compressed) {
|
||||
final int dataLength = readBuffer.readVarInt();
|
||||
final int payloadLength = packetLength - (readBuffer.readerOffset() - readerStart);
|
||||
if (dataLength == 0) {
|
||||
// Data is too small to be compressed, payload is following
|
||||
content = readBuffer;
|
||||
payloadLength = packetLength - (content.readerOffset() - readerStart);
|
||||
decompressedSize = payloadLength;
|
||||
} else {
|
||||
// Decompress to content buffer
|
||||
content = workerContext.contentBuffer;
|
||||
payloadLength = dataLength;
|
||||
final var contentStartMark = content.mark();
|
||||
content = workerContext.contentBuffer.clear();
|
||||
decompressedSize = dataLength;
|
||||
try {
|
||||
final var inflater = workerContext.inflater;
|
||||
inflater.setInput(readBuffer.asByteBuffer(readBuffer.readerOffset(), packetEnd));
|
||||
inflater.inflate(content.asByteBuffer(0, content.capacity()));
|
||||
Inflater inflater = workerContext.inflater;
|
||||
inflater.setInput(readBuffer.asByteBuffer(readBuffer.readerOffset(), payloadLength));
|
||||
inflater.inflate(content.asByteBuffer(0, dataLength));
|
||||
inflater.reset();
|
||||
} catch (DataFormatException e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
content.reset(contentStartMark);
|
||||
}
|
||||
}
|
||||
// Process packet
|
||||
ByteBuffer payload = content.asByteBuffer(content.readerOffset(), payloadLength);
|
||||
ByteBuffer payload = content.asByteBuffer(content.readerOffset(), decompressedSize);
|
||||
final int packetId = Utils.readVarInt(payload);
|
||||
try {
|
||||
packetProcessor.process(this, packetId, payload);
|
||||
@ -162,7 +155,7 @@ public class PlayerSocketConnection extends PlayerConnection {
|
||||
}
|
||||
}
|
||||
// Position buffer to read the next packet
|
||||
readBuffer.reset(packetEnd, limit);
|
||||
readBuffer.readerOffset(readerStart + packetLength);
|
||||
} catch (BufferUnderflowException e) {
|
||||
readBuffer.reset(beginMark);
|
||||
this.cacheBuffer = BinaryBuffer.copy(readBuffer);
|
||||
@ -279,16 +272,17 @@ public class PlayerSocketConnection extends PlayerConnection {
|
||||
if (encrypted) {
|
||||
final Cipher cipher = encryptCipher;
|
||||
// Encrypt data first
|
||||
final int remainingBytes = localBuffer.readableBytes();
|
||||
final byte[] bytes = localBuffer.readRemainingBytes();
|
||||
byte[] outTempArray = new byte[cipher.getOutputSize(remainingBytes)];
|
||||
ByteBuffer cipherInput = localBuffer.asByteBuffer(0, localBuffer.writerOffset());
|
||||
BinaryBuffer pooled = getPooledBuffer();
|
||||
ByteBuffer cipherOutput = pooled.asByteBuffer(0, BUFFER_SIZE);
|
||||
try {
|
||||
cipher.update(bytes, 0, remainingBytes, outTempArray);
|
||||
cipher.update(cipherInput, cipherOutput);
|
||||
} catch (ShortBufferException e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
localBuffer.clear();
|
||||
localBuffer.writeBytes(outTempArray);
|
||||
localBuffer.write(cipherOutput.flip());
|
||||
POOLED_BUFFERS.add(new SoftReference<>(pooled));
|
||||
}
|
||||
|
||||
this.waitingBuffers.add(localBuffer);
|
||||
|
@ -8,6 +8,7 @@ import net.minestom.server.utils.binary.BinaryBuffer;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.SocketChannel;
|
||||
@ -42,7 +43,7 @@ public final class Worker extends Thread {
|
||||
if (!key.isReadable()) return;
|
||||
PlayerSocketConnection connection = connectionMap.get(channel);
|
||||
try {
|
||||
var readBuffer = context.readBuffer;
|
||||
BinaryBuffer readBuffer = context.readBuffer.clear();
|
||||
// Consume last incomplete packet
|
||||
connection.consumeCache(readBuffer);
|
||||
// Read & process
|
||||
@ -51,8 +52,6 @@ public final class Worker extends Thread {
|
||||
} catch (IOException e) {
|
||||
// TODO print exception? (should ignore disconnection)
|
||||
connection.disconnect();
|
||||
} finally {
|
||||
context.clearBuffers();
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
@ -80,7 +79,7 @@ public final class Worker extends Thread {
|
||||
this.connectionMap.put(channel, new PlayerSocketConnection(this, channel, channel.getRemoteAddress()));
|
||||
channel.configureBlocking(false);
|
||||
channel.register(selector, SelectionKey.OP_READ);
|
||||
var socket = channel.socket();
|
||||
Socket socket = channel.socket();
|
||||
socket.setSendBufferSize(Server.SOCKET_SEND_BUFFER_SIZE);
|
||||
socket.setReceiveBufferSize(Server.SOCKET_RECEIVE_BUFFER_SIZE);
|
||||
socket.setTcpNoDelay(Server.NO_DELAY);
|
||||
@ -95,10 +94,5 @@ public final class Worker extends Thread {
|
||||
public final BinaryBuffer readBuffer = BinaryBuffer.ofSize(Server.MAX_PACKET_SIZE);
|
||||
public final BinaryBuffer contentBuffer = BinaryBuffer.ofSize(Server.MAX_PACKET_SIZE);
|
||||
public final Inflater inflater = new Inflater();
|
||||
|
||||
void clearBuffers() {
|
||||
this.readBuffer.clear();
|
||||
this.contentBuffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
package net.minestom.server.thread;
|
||||
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Each {@link Chunk} gets assigned to a random thread.
|
||||
*/
|
||||
public class PerChunkThreadProvider extends ThreadProvider {
|
||||
|
||||
public PerChunkThreadProvider(int threadCount) {
|
||||
super(threadCount);
|
||||
}
|
||||
|
||||
public PerChunkThreadProvider() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findThread(@NotNull Chunk chunk) {
|
||||
return chunk.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull RefreshType getChunkRefreshType() {
|
||||
return RefreshType.NEVER;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package net.minestom.server.thread;
|
||||
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Each {@link Instance} gets assigned to a random thread.
|
||||
*/
|
||||
public class PerInstanceThreadProvider extends ThreadProvider {
|
||||
|
||||
public PerInstanceThreadProvider(int threadCount) {
|
||||
super(threadCount);
|
||||
}
|
||||
|
||||
public PerInstanceThreadProvider() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findThread(@NotNull Chunk chunk) {
|
||||
return chunk.getInstance().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull RefreshType getChunkRefreshType() {
|
||||
return RefreshType.NEVER;
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package net.minestom.server.thread;
|
||||
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Uses a single thread for all chunks.
|
||||
*/
|
||||
public class SingleThreadProvider extends ThreadProvider {
|
||||
|
||||
public SingleThreadProvider() {
|
||||
super(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findThread(@NotNull Chunk chunk) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull RefreshType getChunkRefreshType() {
|
||||
return RefreshType.NEVER;
|
||||
}
|
||||
}
|
324
src/main/java/net/minestom/server/thread/ThreadDispatcher.java
Normal file
324
src/main/java/net/minestom/server/thread/ThreadDispatcher.java
Normal file
@ -0,0 +1,324 @@
|
||||
package net.minestom.server.thread;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.acquirable.Acquirable;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.utils.MathUtils;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Phaser;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Used to link chunks into multiple groups.
|
||||
* Then executed into a thread pool.
|
||||
*/
|
||||
public final class ThreadDispatcher {
|
||||
private final ThreadProvider provider;
|
||||
private final List<TickThread> threads;
|
||||
|
||||
private final Map<TickThread, Set<ChunkEntry>> threadChunkMap = new HashMap<>();
|
||||
private final Map<Chunk, ChunkEntry> chunkEntryMap = new HashMap<>();
|
||||
private final ArrayDeque<Chunk> chunkUpdateQueue = new ArrayDeque<>();
|
||||
|
||||
private final Queue<Chunk> chunkLoadRequests = new ConcurrentLinkedQueue<>();
|
||||
private final Queue<Chunk> chunkUnloadRequests = new ConcurrentLinkedQueue<>();
|
||||
private final Queue<Entity> entityUpdateRequests = new ConcurrentLinkedQueue<>();
|
||||
private final Queue<Entity> entityRemovalRequests = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final Phaser phaser = new Phaser(1);
|
||||
|
||||
private ThreadDispatcher(ThreadProvider provider, int threadCount) {
|
||||
this.provider = provider;
|
||||
this.threads = new ArrayList<>(threadCount);
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
final TickThread tickThread = new TickThread(phaser, i);
|
||||
this.threads.add(tickThread);
|
||||
tickThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull ThreadDispatcher of(@NotNull ThreadProvider provider, int threadCount) {
|
||||
return new ThreadDispatcher(provider, threadCount);
|
||||
}
|
||||
|
||||
public static @NotNull ThreadDispatcher singleThread() {
|
||||
return of(ThreadProvider.SINGLE, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the maximum percentage of tick time that can be spent refreshing chunks thread.
|
||||
* <p>
|
||||
* Percentage based on {@link MinecraftServer#TICK_MS}.
|
||||
*
|
||||
* @return the refresh percentage
|
||||
*/
|
||||
public float getRefreshPercentage() {
|
||||
return 0.3f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum time used to refresh chunks and entities thread.
|
||||
*
|
||||
* @return the minimum refresh time in milliseconds
|
||||
*/
|
||||
public int getMinimumRefreshTime() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum time used to refresh chunks and entities thread.
|
||||
*
|
||||
* @return the maximum refresh time in milliseconds
|
||||
*/
|
||||
public int getMaximumRefreshTime() {
|
||||
return (int) (MinecraftServer.TICK_MS * 0.3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the update by creating the {@link TickThread} tasks.
|
||||
*
|
||||
* @param time the tick time in milliseconds
|
||||
*/
|
||||
public void updateAndAwait(long time) {
|
||||
for (var entry : threadChunkMap.entrySet()) {
|
||||
final TickThread thread = entry.getKey();
|
||||
final Set<ChunkEntry> chunkEntries = entry.getValue();
|
||||
if (chunkEntries == null || chunkEntries.isEmpty()) {
|
||||
// Nothing to tick
|
||||
continue;
|
||||
}
|
||||
// Execute tick
|
||||
this.phaser.register();
|
||||
thread.startTick(() -> {
|
||||
Acquirable.refreshEntries(chunkEntries);
|
||||
|
||||
final ReentrantLock lock = thread.getLock();
|
||||
lock.lock();
|
||||
for (ChunkEntry chunkEntry : chunkEntries) {
|
||||
final Chunk chunk = chunkEntry.chunk;
|
||||
if (!ChunkUtils.isLoaded(chunk)) return;
|
||||
try {
|
||||
chunk.tick(time);
|
||||
} catch (Throwable e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
final List<Entity> entities = chunkEntry.entities;
|
||||
if (!entities.isEmpty()) {
|
||||
for (Entity entity : entities) {
|
||||
if (lock.hasQueuedThreads()) {
|
||||
lock.unlock();
|
||||
// #acquire() callbacks should be called here
|
||||
lock.lock();
|
||||
}
|
||||
try {
|
||||
entity.tick(time);
|
||||
} catch (Throwable e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
// #acquire() callbacks
|
||||
});
|
||||
}
|
||||
this.phaser.arriveAndAwaitAdvance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of each tick to clear removed entities,
|
||||
* refresh the chunk linked to an entity, and chunk threads based on {@link ThreadProvider#findThread(Chunk)}.
|
||||
*
|
||||
* @param tickTime the duration of the tick in ms,
|
||||
* used to ensure that the refresh does not take more time than the tick itself
|
||||
*/
|
||||
public void refreshThreads(long tickTime) {
|
||||
processLoadedChunks();
|
||||
processUnloadedChunks();
|
||||
processRemovedEntities();
|
||||
processUpdatedEntities();
|
||||
if (provider.getChunkRefreshType() == ThreadProvider.RefreshType.NEVER)
|
||||
return;
|
||||
|
||||
final int timeOffset = MathUtils.clamp((int) ((double) tickTime * getRefreshPercentage()),
|
||||
getMinimumRefreshTime(), getMaximumRefreshTime());
|
||||
final long endTime = System.currentTimeMillis() + timeOffset;
|
||||
final int size = chunkUpdateQueue.size();
|
||||
int counter = 0;
|
||||
while (true) {
|
||||
final Chunk chunk = chunkUpdateQueue.pollFirst();
|
||||
if (!ChunkUtils.isLoaded(chunk)) {
|
||||
removeChunk(chunk);
|
||||
continue;
|
||||
}
|
||||
// Update chunk threads
|
||||
switchChunk(chunk);
|
||||
// Add back to the deque
|
||||
chunkUpdateQueue.addLast(chunk);
|
||||
|
||||
if (++counter > size || System.currentTimeMillis() >= endTime)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdowns all the {@link TickThread tick threads}.
|
||||
* <p>
|
||||
* Action is irreversible.
|
||||
*/
|
||||
public void shutdown() {
|
||||
this.threads.forEach(TickThread::shutdown);
|
||||
}
|
||||
|
||||
public void onInstanceCreate(@NotNull Instance instance) {
|
||||
instance.getChunks().forEach(this::onChunkLoad);
|
||||
}
|
||||
|
||||
public void onInstanceDelete(@NotNull Instance instance) {
|
||||
instance.getChunks().forEach(this::onChunkUnload);
|
||||
}
|
||||
|
||||
public void onChunkLoad(Chunk chunk) {
|
||||
this.chunkLoadRequests.add(chunk);
|
||||
}
|
||||
|
||||
public void onChunkUnload(Chunk chunk) {
|
||||
this.chunkUnloadRequests.add(chunk);
|
||||
}
|
||||
|
||||
public void updateEntity(@NotNull Entity entity) {
|
||||
this.entityUpdateRequests.add(entity);
|
||||
}
|
||||
|
||||
public void removeEntity(@NotNull Entity entity) {
|
||||
this.entityRemovalRequests.add(entity);
|
||||
}
|
||||
|
||||
private void switchChunk(@NotNull Chunk chunk) {
|
||||
ChunkEntry chunkEntry = chunkEntryMap.get(chunk);
|
||||
if (chunkEntry == null) return;
|
||||
Set<ChunkEntry> chunks = threadChunkMap.get(chunkEntry.thread);
|
||||
if (chunks == null || chunks.isEmpty()) return;
|
||||
chunks.remove(chunkEntry);
|
||||
setChunkThread(chunk, tickThread -> {
|
||||
chunkEntry.thread = tickThread;
|
||||
return chunkEntry;
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull ChunkEntry setChunkThread(@NotNull Chunk chunk,
|
||||
@NotNull Function<TickThread, ChunkEntry> chunkEntrySupplier) {
|
||||
final int threadId = Math.abs(provider.findThread(chunk)) % threads.size();
|
||||
TickThread thread = threads.get(threadId);
|
||||
Set<ChunkEntry> chunks = threadChunkMap.computeIfAbsent(thread, tickThread -> new HashSet<>());
|
||||
|
||||
ChunkEntry chunkEntry = chunkEntrySupplier.apply(thread);
|
||||
chunks.add(chunkEntry);
|
||||
return chunkEntry;
|
||||
}
|
||||
|
||||
private void removeChunk(Chunk chunk) {
|
||||
ChunkEntry chunkEntry = chunkEntryMap.get(chunk);
|
||||
if (chunkEntry != null) {
|
||||
TickThread thread = chunkEntry.thread;
|
||||
Set<ChunkEntry> chunks = threadChunkMap.get(thread);
|
||||
if (chunks != null) {
|
||||
chunks.remove(chunkEntry);
|
||||
}
|
||||
chunkEntryMap.remove(chunk);
|
||||
}
|
||||
this.chunkUpdateQueue.remove(chunk);
|
||||
}
|
||||
|
||||
private void processLoadedChunks() {
|
||||
Chunk chunk;
|
||||
while ((chunk = chunkLoadRequests.poll()) != null) {
|
||||
Chunk finalChunk = chunk;
|
||||
ChunkEntry chunkEntry = setChunkThread(chunk, (thread) -> new ChunkEntry(thread, finalChunk));
|
||||
this.chunkEntryMap.put(chunk, chunkEntry);
|
||||
this.chunkUpdateQueue.add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void processUnloadedChunks() {
|
||||
Chunk chunk;
|
||||
while ((chunk = chunkUnloadRequests.poll()) != null) {
|
||||
removeChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void processRemovedEntities() {
|
||||
Entity entity;
|
||||
while ((entity = entityRemovalRequests.poll()) != null) {
|
||||
var acquirableEntity = entity.getAcquirable();
|
||||
ChunkEntry chunkEntry = acquirableEntity.getHandler().getChunkEntry();
|
||||
if (chunkEntry != null) {
|
||||
chunkEntry.entities.remove(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processUpdatedEntities() {
|
||||
Entity entity;
|
||||
while ((entity = entityUpdateRequests.poll()) != null) {
|
||||
ChunkEntry chunkEntry;
|
||||
|
||||
var acquirableEntity = entity.getAcquirable();
|
||||
chunkEntry = acquirableEntity.getHandler().getChunkEntry();
|
||||
// Remove from previous list
|
||||
if (chunkEntry != null) {
|
||||
chunkEntry.entities.remove(entity);
|
||||
}
|
||||
// Add to new list
|
||||
chunkEntry = chunkEntryMap.get(entity.getChunk());
|
||||
if (chunkEntry != null) {
|
||||
chunkEntry.entities.add(entity);
|
||||
acquirableEntity.getHandler().refreshChunkEntry(chunkEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ChunkEntry {
|
||||
private volatile TickThread thread;
|
||||
private final Chunk chunk;
|
||||
private final List<Entity> entities = new ArrayList<>();
|
||||
|
||||
private ChunkEntry(TickThread thread, Chunk chunk) {
|
||||
this.thread = thread;
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
public @NotNull TickThread getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public @NotNull Chunk getChunk() {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
public @NotNull List<Entity> getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChunkEntry that = (ChunkEntry) o;
|
||||
return chunk.equals(that.chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(chunk);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,318 +1,36 @@
|
||||
package net.minestom.server.thread;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.acquirable.Acquirable;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.utils.MathUtils;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Phaser;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Used to link chunks into multiple groups.
|
||||
* Then executed into a thread pool.
|
||||
*/
|
||||
public abstract class ThreadProvider {
|
||||
|
||||
private final List<TickThread> threads;
|
||||
|
||||
private final Map<TickThread, Set<ChunkEntry>> threadChunkMap = new HashMap<>();
|
||||
private final Map<Chunk, ChunkEntry> chunkEntryMap = new HashMap<>();
|
||||
// Iterated over to refresh the thread used to update entities & chunks
|
||||
private final ArrayDeque<Chunk> chunks = new ArrayDeque<>();
|
||||
private final Set<Entity> updatableEntities = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Entity> removedEntities = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Phaser phaser = new Phaser(1);
|
||||
|
||||
public ThreadProvider(int threadCount) {
|
||||
this.threads = new ArrayList<>(threadCount);
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
final TickThread.BatchRunnable batchRunnable = new TickThread.BatchRunnable();
|
||||
final TickThread tickThread = new TickThread(batchRunnable, i);
|
||||
this.threads.add(tickThread);
|
||||
|
||||
tickThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
public ThreadProvider() {
|
||||
this(Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
|
||||
public synchronized void onInstanceCreate(@NotNull Instance instance) {
|
||||
instance.getChunks().forEach(this::addChunk);
|
||||
}
|
||||
|
||||
public synchronized void onInstanceDelete(@NotNull Instance instance) {
|
||||
instance.getChunks().forEach(this::removeChunk);
|
||||
}
|
||||
|
||||
public synchronized void onChunkLoad(Chunk chunk) {
|
||||
addChunk(chunk);
|
||||
}
|
||||
|
||||
public synchronized void onChunkUnload(Chunk chunk) {
|
||||
removeChunk(chunk);
|
||||
}
|
||||
@FunctionalInterface
|
||||
@ApiStatus.Experimental
|
||||
public interface ThreadProvider {
|
||||
ThreadProvider PER_CHUNk = Object::hashCode;
|
||||
ThreadProvider PER_INSTANCE = chunk -> chunk.getInstance().hashCode();
|
||||
ThreadProvider SINGLE = chunk -> 0;
|
||||
|
||||
/**
|
||||
* Performs a server tick for all chunks based on their linked thread.
|
||||
*
|
||||
* @param chunk the chunk
|
||||
*/
|
||||
public abstract int findThread(@NotNull Chunk chunk);
|
||||
int findThread(@NotNull Chunk chunk);
|
||||
|
||||
/**
|
||||
* Defines how often chunks thread should be updated.
|
||||
*
|
||||
* @return the refresh type
|
||||
*/
|
||||
public @NotNull RefreshType getChunkRefreshType() {
|
||||
return RefreshType.CONSTANT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the maximum percentage of tick time that can be spent refreshing chunks thread.
|
||||
* <p>
|
||||
* Percentage based on {@link MinecraftServer#TICK_MS}.
|
||||
*
|
||||
* @return the refresh percentage
|
||||
*/
|
||||
public float getRefreshPercentage() {
|
||||
return 0.3f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum time used to refresh chunks and entities thread.
|
||||
*
|
||||
* @return the minimum refresh time in milliseconds
|
||||
*/
|
||||
public int getMinimumRefreshTime() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum time used to refresh chunks and entities thread.
|
||||
*
|
||||
* @return the maximum refresh time in milliseconds
|
||||
*/
|
||||
public int getMaximumRefreshTime() {
|
||||
return (int) (MinecraftServer.TICK_MS * 0.3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the update by creating the {@link TickThread} tasks.
|
||||
*
|
||||
* @param time the tick time in milliseconds
|
||||
*/
|
||||
public void updateAndAwait(long time) {
|
||||
for (var entry : threadChunkMap.entrySet()) {
|
||||
final TickThread thread = entry.getKey();
|
||||
final var chunkEntries = entry.getValue();
|
||||
if (chunkEntries == null || chunkEntries.isEmpty()) {
|
||||
// Nothing to tick
|
||||
continue;
|
||||
}
|
||||
// Execute tick
|
||||
this.phaser.register();
|
||||
thread.runnable.startTick(phaser, () -> {
|
||||
Acquirable.refreshEntries(chunkEntries);
|
||||
|
||||
final ReentrantLock lock = thread.getLock();
|
||||
lock.lock();
|
||||
for (var chunkEntry : chunkEntries) {
|
||||
final Chunk chunk = chunkEntry.chunk;
|
||||
if (!ChunkUtils.isLoaded(chunk))
|
||||
return;
|
||||
try {
|
||||
chunk.tick(time);
|
||||
} catch (Throwable e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
final var entities = chunkEntry.entities;
|
||||
if (!entities.isEmpty()) {
|
||||
for (Entity entity : entities) {
|
||||
if (lock.hasQueuedThreads()) {
|
||||
lock.unlock();
|
||||
// #acquire() callbacks should be called here
|
||||
lock.lock();
|
||||
}
|
||||
try {
|
||||
entity.tick(time);
|
||||
} catch (Throwable e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
// #acquire() callbacks
|
||||
});
|
||||
}
|
||||
this.phaser.arriveAndAwaitAdvance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of each tick to clear removed entities,
|
||||
* refresh the chunk linked to an entity, and chunk threads based on {@link #findThread(Chunk)}.
|
||||
*
|
||||
* @param tickTime the duration of the tick in ms,
|
||||
* used to ensure that the refresh does not take more time than the tick itself
|
||||
*/
|
||||
public synchronized void refreshThreads(long tickTime) {
|
||||
// Clear removed entities
|
||||
processRemovedEntities();
|
||||
// Update entities chunks
|
||||
processUpdatedEntities();
|
||||
if (getChunkRefreshType() == RefreshType.NEVER)
|
||||
return;
|
||||
|
||||
final int timeOffset = MathUtils.clamp((int) ((double) tickTime * getRefreshPercentage()),
|
||||
getMinimumRefreshTime(), getMaximumRefreshTime());
|
||||
final long endTime = System.currentTimeMillis() + timeOffset;
|
||||
final int size = chunks.size();
|
||||
int counter = 0;
|
||||
while (true) {
|
||||
final Chunk chunk = chunks.pollFirst();
|
||||
if (!ChunkUtils.isLoaded(chunk)) {
|
||||
removeChunk(chunk);
|
||||
continue;
|
||||
}
|
||||
// Update chunk threads
|
||||
switchChunk(chunk);
|
||||
// Add back to the deque
|
||||
chunks.addLast(chunk);
|
||||
|
||||
if (++counter > size)
|
||||
break;
|
||||
if (System.currentTimeMillis() >= endTime)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entity into the waiting list to get assigned in a thread.
|
||||
* <p>
|
||||
* Called when entering a new chunk.
|
||||
*
|
||||
* @param entity the entity to add
|
||||
*/
|
||||
public void updateEntity(@NotNull Entity entity) {
|
||||
this.updatableEntities.add(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entity into the waiting list to get removed from its thread.
|
||||
* <p>
|
||||
* Called in {@link Entity#remove()}.
|
||||
*
|
||||
* @param entity the entity to remove
|
||||
*/
|
||||
public void removeEntity(@NotNull Entity entity) {
|
||||
this.removedEntities.add(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdowns all the {@link TickThread tick threads}.
|
||||
* <p>
|
||||
* Action is irreversible.
|
||||
*/
|
||||
public void shutdown() {
|
||||
this.threads.forEach(TickThread::shutdown);
|
||||
}
|
||||
|
||||
private void addChunk(@NotNull Chunk chunk) {
|
||||
ChunkEntry chunkEntry = setChunkThread(chunk, (thread) -> new ChunkEntry(thread, chunk));
|
||||
this.chunkEntryMap.put(chunk, chunkEntry);
|
||||
this.chunks.add(chunk);
|
||||
}
|
||||
|
||||
private void switchChunk(@NotNull Chunk chunk) {
|
||||
ChunkEntry chunkEntry = chunkEntryMap.get(chunk);
|
||||
if (chunkEntry == null)
|
||||
return;
|
||||
var chunks = threadChunkMap.get(chunkEntry.thread);
|
||||
if (chunks == null || chunks.isEmpty())
|
||||
return;
|
||||
chunks.remove(chunkEntry);
|
||||
|
||||
setChunkThread(chunk, tickThread -> {
|
||||
chunkEntry.thread = tickThread;
|
||||
return chunkEntry;
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull ChunkEntry setChunkThread(@NotNull Chunk chunk,
|
||||
@NotNull Function<TickThread, ChunkEntry> chunkEntrySupplier) {
|
||||
final int threadId = Math.abs(findThread(chunk)) % threads.size();
|
||||
TickThread thread = threads.get(threadId);
|
||||
var chunks = threadChunkMap.computeIfAbsent(thread, tickThread -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
ChunkEntry chunkEntry = chunkEntrySupplier.apply(thread);
|
||||
chunks.add(chunkEntry);
|
||||
return chunkEntry;
|
||||
}
|
||||
|
||||
private void removeChunk(Chunk chunk) {
|
||||
ChunkEntry chunkEntry = chunkEntryMap.get(chunk);
|
||||
if (chunkEntry != null) {
|
||||
TickThread thread = chunkEntry.thread;
|
||||
var chunks = threadChunkMap.get(thread);
|
||||
if (chunks != null) {
|
||||
chunks.remove(chunkEntry);
|
||||
}
|
||||
chunkEntryMap.remove(chunk);
|
||||
}
|
||||
this.chunks.remove(chunk);
|
||||
}
|
||||
|
||||
private void processRemovedEntities() {
|
||||
if (removedEntities.isEmpty())
|
||||
return;
|
||||
for (Entity entity : removedEntities) {
|
||||
var acquirableEntity = entity.getAcquirable();
|
||||
ChunkEntry chunkEntry = acquirableEntity.getHandler().getChunkEntry();
|
||||
// Remove from list
|
||||
if (chunkEntry != null) {
|
||||
chunkEntry.entities.remove(entity);
|
||||
}
|
||||
}
|
||||
this.removedEntities.clear();
|
||||
}
|
||||
|
||||
private void processUpdatedEntities() {
|
||||
if (updatableEntities.isEmpty())
|
||||
return;
|
||||
for (Entity entity : updatableEntities) {
|
||||
var acquirableEntity = entity.getAcquirable();
|
||||
ChunkEntry handlerChunkEntry = acquirableEntity.getHandler().getChunkEntry();
|
||||
// Remove from previous list
|
||||
if (handlerChunkEntry != null) {
|
||||
handlerChunkEntry.entities.remove(entity);
|
||||
}
|
||||
// Add to new list
|
||||
ChunkEntry chunkEntry = chunkEntryMap.get(entity.getChunk());
|
||||
if (chunkEntry != null) {
|
||||
chunkEntry.entities.add(entity);
|
||||
acquirableEntity.getHandler().refreshChunkEntry(chunkEntry);
|
||||
}
|
||||
}
|
||||
this.updatableEntities.clear();
|
||||
default @NotNull RefreshType getChunkRefreshType() {
|
||||
return RefreshType.NEVER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how often chunks thread should be refreshed.
|
||||
*/
|
||||
public enum RefreshType {
|
||||
enum RefreshType {
|
||||
/**
|
||||
* Chunk thread is constant after being defined.
|
||||
*/
|
||||
@ -326,40 +44,4 @@ public abstract class ThreadProvider {
|
||||
*/
|
||||
RARELY
|
||||
}
|
||||
|
||||
public static class ChunkEntry {
|
||||
private volatile TickThread thread;
|
||||
private final Chunk chunk;
|
||||
private final List<Entity> entities = new ArrayList<>();
|
||||
|
||||
private ChunkEntry(TickThread thread, Chunk chunk) {
|
||||
this.thread = thread;
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
public @NotNull TickThread getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public @NotNull Chunk getChunk() {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
public @NotNull List<Entity> getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChunkEntry that = (ChunkEntry) o;
|
||||
return chunk.equals(that.chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,44 @@
|
||||
package net.minestom.server.thread;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.Phaser;
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Thread responsible for ticking {@link net.minestom.server.instance.Chunk chunks} and {@link net.minestom.server.entity.Entity entities}.
|
||||
* <p>
|
||||
* Created in {@link ThreadProvider}, and awaken every tick with a task to execute.
|
||||
* Created in {@link ThreadDispatcher}, and awaken every tick with a task to execute.
|
||||
*/
|
||||
public class TickThread extends Thread {
|
||||
|
||||
protected final BatchRunnable runnable;
|
||||
public final class TickThread extends Thread {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Phaser phaser;
|
||||
private volatile boolean stop;
|
||||
private Runnable tickRunnable;
|
||||
|
||||
public TickThread(@NotNull BatchRunnable runnable, int number) {
|
||||
super(runnable, MinecraftServer.THREAD_NAME_TICK + "-" + number);
|
||||
this.runnable = runnable;
|
||||
public TickThread(Phaser phaser, int number) {
|
||||
super(MinecraftServer.THREAD_NAME_TICK + "-" + number);
|
||||
this.phaser = phaser;
|
||||
}
|
||||
|
||||
this.runnable.setLinkedThread(this);
|
||||
@Override
|
||||
public void run() {
|
||||
while (!stop) {
|
||||
final Runnable localRunnable = tickRunnable;
|
||||
if (localRunnable != null) {
|
||||
localRunnable.run();
|
||||
this.tickRunnable = null;
|
||||
this.phaser.arriveAndDeregister();
|
||||
}
|
||||
LockSupport.park(this);
|
||||
}
|
||||
}
|
||||
|
||||
void startTick(@NotNull Runnable runnable) {
|
||||
this.tickRunnable = runnable;
|
||||
LockSupport.unpark(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,52 +54,7 @@ public class TickThread extends Thread {
|
||||
* Shutdowns the thread. Cannot be undone.
|
||||
*/
|
||||
public void shutdown() {
|
||||
this.runnable.stop = true;
|
||||
this.stop = true;
|
||||
LockSupport.unpark(this);
|
||||
}
|
||||
|
||||
protected static class BatchRunnable implements Runnable {
|
||||
private static final AtomicReferenceFieldUpdater<BatchRunnable, TickContext> CONTEXT_UPDATER =
|
||||
AtomicReferenceFieldUpdater.newUpdater(BatchRunnable.class, TickContext.class, "tickContext");
|
||||
|
||||
private volatile boolean stop;
|
||||
private TickThread tickThread;
|
||||
|
||||
private volatile TickContext tickContext;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Check.notNull(tickThread, "The linked BatchThread cannot be null!");
|
||||
while (!stop) {
|
||||
final TickContext localContext = tickContext;
|
||||
// The context is necessary to control the tick rates
|
||||
if (localContext != null) {
|
||||
// Execute tick
|
||||
CONTEXT_UPDATER.compareAndSet(this, localContext, null);
|
||||
localContext.runnable.run();
|
||||
localContext.phaser.arriveAndDeregister();
|
||||
}
|
||||
LockSupport.park(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected void startTick(@NotNull Phaser phaser, @NotNull Runnable runnable) {
|
||||
this.tickContext = new TickContext(phaser, runnable);
|
||||
LockSupport.unpark(tickThread);
|
||||
}
|
||||
|
||||
private void setLinkedThread(TickThread tickThread) {
|
||||
this.tickThread = tickThread;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TickContext {
|
||||
private final Phaser phaser;
|
||||
private final Runnable runnable;
|
||||
|
||||
private TickContext(@NotNull Phaser phaser, @NotNull Runnable runnable) {
|
||||
this.phaser = phaser;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,9 @@ import java.util.zip.Deflater;
|
||||
*/
|
||||
public final class PacketUtils {
|
||||
private static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
|
||||
private static final ThreadLocal<Deflater> COMPRESSOR = ThreadLocal.withInitial(Deflater::new);
|
||||
private static final ThreadLocal<Deflater> LOCAL_DEFLATER = ThreadLocal.withInitial(Deflater::new);
|
||||
private static final LocalCache PACKET_BUFFER = LocalCache.get("packet-buffer", Server.MAX_PACKET_SIZE);
|
||||
private static final LocalCache COMPRESSION_CACHE = LocalCache.get("compression-buffer", Server.MAX_PACKET_SIZE);
|
||||
private static final LocalCache LOCAL_BUFFER = LocalCache.get("local-buffer", Server.MAX_PACKET_SIZE);
|
||||
|
||||
private static final Object VIEWABLE_PACKET_LOCK = new Object();
|
||||
private static final Map<Viewable, ViewableStorage> VIEWABLE_STORAGE_MAP = new WeakHashMap<>();
|
||||
@ -51,7 +51,7 @@ public final class PacketUtils {
|
||||
@ApiStatus.Internal
|
||||
@ApiStatus.Experimental
|
||||
public static ByteBuffer localBuffer() {
|
||||
return COMPRESSION_CACHE.get();
|
||||
return LOCAL_BUFFER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,7 +179,7 @@ public final class PacketUtils {
|
||||
@NotNull ServerPacket packet,
|
||||
boolean compression) {
|
||||
if (!compression) {
|
||||
// Length + payload
|
||||
// Uncompressed format https://wiki.vg/Protocol#Without_compression
|
||||
final int lengthIndex = Utils.writeEmptyVarIntHeader(buffer);
|
||||
Utils.writeVarInt(buffer, packet.getId());
|
||||
packet.write(new BinaryWriter(buffer));
|
||||
@ -187,13 +187,13 @@ public final class PacketUtils {
|
||||
Utils.writeVarIntHeader(buffer, lengthIndex, finalSize);
|
||||
return;
|
||||
}
|
||||
// Compressed format
|
||||
// Compressed format https://wiki.vg/Protocol#With_compression
|
||||
final int compressedIndex = Utils.writeEmptyVarIntHeader(buffer);
|
||||
final int uncompressedIndex = Utils.writeEmptyVarIntHeader(buffer);
|
||||
final int contentStart = buffer.position();
|
||||
|
||||
final int contentStart = buffer.position();
|
||||
Utils.writeVarInt(buffer, packet.getId());
|
||||
packet.write(new BinaryWriter(buffer));
|
||||
packet.write(BinaryWriter.view(buffer)); // ensure that the buffer is not resized/changed
|
||||
final int packetSize = buffer.position() - contentStart;
|
||||
if (packetSize >= MinecraftServer.getCompressionThreshold()) {
|
||||
// Packet large enough, compress
|
||||
@ -201,17 +201,18 @@ public final class PacketUtils {
|
||||
final ByteBuffer uncompressedContent = buffer.slice().limit(packetSize);
|
||||
final ByteBuffer uncompressedCopy = localBuffer().put(uncompressedContent).flip();
|
||||
|
||||
Deflater deflater = COMPRESSOR.get();
|
||||
Deflater deflater = LOCAL_DEFLATER.get();
|
||||
deflater.setInput(uncompressedCopy);
|
||||
deflater.finish();
|
||||
deflater.deflate(buffer);
|
||||
deflater.reset();
|
||||
|
||||
Utils.writeVarIntHeader(buffer, compressedIndex, (buffer.position() - contentStart) + 3);
|
||||
Utils.writeVarIntHeader(buffer, uncompressedIndex, packetSize);
|
||||
Utils.writeVarIntHeader(buffer, compressedIndex, buffer.position() - uncompressedIndex);
|
||||
Utils.writeVarIntHeader(buffer, uncompressedIndex, packetSize); // Data Length
|
||||
} else {
|
||||
Utils.writeVarIntHeader(buffer, compressedIndex, packetSize + 3);
|
||||
Utils.writeVarIntHeader(buffer, uncompressedIndex, 0);
|
||||
// Packet too small
|
||||
Utils.writeVarIntHeader(buffer, compressedIndex, buffer.position() - uncompressedIndex);
|
||||
Utils.writeVarIntHeader(buffer, uncompressedIndex, 0); // Data Length (0 since uncompressed)
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,7 +247,7 @@ public final class PacketUtils {
|
||||
}
|
||||
|
||||
public static LocalCache get(String name, int size) {
|
||||
return CACHES.computeIfAbsent(name, s -> new LocalCache(name, size));
|
||||
return CACHES.computeIfAbsent(name, s -> new LocalCache(s, size));
|
||||
}
|
||||
|
||||
public String name() {
|
||||
|
@ -42,12 +42,9 @@ public final class Utils {
|
||||
}
|
||||
|
||||
public static void writeVarIntHeader(@NotNull ByteBuffer buffer, int startIndex, int value) {
|
||||
final int indexCache = buffer.position();
|
||||
buffer.position(startIndex);
|
||||
buffer.put((byte) (value & 0x7F | 0x80));
|
||||
buffer.put((byte) ((value >>> 7) & 0x7F | 0x80));
|
||||
buffer.put((byte) (value >>> 14));
|
||||
buffer.position(indexCache);
|
||||
buffer.put(startIndex, (byte) (value & 0x7F | 0x80));
|
||||
buffer.put(startIndex + 1, (byte) ((value >>> 7) & 0x7F | 0x80));
|
||||
buffer.put(startIndex + 2, (byte) (value >>> 14));
|
||||
}
|
||||
|
||||
public static int writeEmptyVarIntHeader(@NotNull ByteBuffer buffer) {
|
||||
|
@ -78,6 +78,10 @@ public final class BinaryBuffer {
|
||||
reset(marker.readerOffset(), marker.writerOffset());
|
||||
}
|
||||
|
||||
public boolean canRead(int size) {
|
||||
return readerOffset + size < capacity;
|
||||
}
|
||||
|
||||
public boolean canWrite(int size) {
|
||||
return writerOffset + size < capacity;
|
||||
}
|
||||
@ -90,6 +94,10 @@ public final class BinaryBuffer {
|
||||
return readerOffset;
|
||||
}
|
||||
|
||||
public void readerOffset(int offset) {
|
||||
this.readerOffset = offset;
|
||||
}
|
||||
|
||||
public int writerOffset() {
|
||||
return writerOffset;
|
||||
}
|
||||
@ -114,9 +122,10 @@ public final class BinaryBuffer {
|
||||
return readBytes(readableBytes());
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
public BinaryBuffer clear() {
|
||||
this.readerOffset = 0;
|
||||
this.writerOffset = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ByteBuffer asByteBuffer(int reader, int writer) {
|
||||
|
@ -7,6 +7,7 @@ import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.utils.SerializerUtils;
|
||||
import net.minestom.server.utils.Utils;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jglrxavpok.hephaistos.nbt.NBT;
|
||||
import org.jglrxavpok.hephaistos.nbt.NBTWriter;
|
||||
@ -29,32 +30,33 @@ public class BinaryWriter extends OutputStream {
|
||||
private ByteBuffer buffer;
|
||||
private NBTWriter nbtWriter; // Lazily initialized
|
||||
|
||||
/**
|
||||
* Creates a {@link BinaryWriter} using a heap buffer with a custom initial capacity.
|
||||
*
|
||||
* @param initialCapacity the initial capacity of the binary writer
|
||||
*/
|
||||
public BinaryWriter(int initialCapacity) {
|
||||
this.buffer = ByteBuffer.allocate(initialCapacity);
|
||||
private final boolean resizable;
|
||||
|
||||
private BinaryWriter(ByteBuffer buffer, boolean resizable) {
|
||||
this.buffer = buffer;
|
||||
this.resizable = resizable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BinaryWriter} from multiple a single buffer.
|
||||
*
|
||||
* @param buffer the writer buffer
|
||||
*/
|
||||
public BinaryWriter(@NotNull ByteBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
this.resizable = true;
|
||||
}
|
||||
|
||||
public BinaryWriter(int initialCapacity) {
|
||||
this(ByteBuffer.allocate(initialCapacity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BinaryWriter} with a "reasonably small initial capacity".
|
||||
*/
|
||||
public BinaryWriter() {
|
||||
this(255);
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
public static BinaryWriter view(ByteBuffer buffer) {
|
||||
return new BinaryWriter(buffer, false);
|
||||
}
|
||||
|
||||
protected void ensureSize(int length) {
|
||||
if (!resizable) return;
|
||||
final int position = buffer.position();
|
||||
if (position + length >= buffer.limit()) {
|
||||
final int newLength = (position + length) * 4;
|
||||
|
@ -54,12 +54,6 @@ public final class ChunkUtils {
|
||||
return completableFuture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if a chunk is loaded.
|
||||
*
|
||||
* @param chunk the chunk to check
|
||||
* @return true if the chunk is loaded, false otherwise
|
||||
*/
|
||||
public static boolean isLoaded(@Nullable Chunk chunk) {
|
||||
return chunk != null && chunk.isLoaded();
|
||||
}
|
||||
@ -99,8 +93,8 @@ public final class ChunkUtils {
|
||||
* @return the chunk X or Z based on the argument
|
||||
*/
|
||||
public static int getChunkCoordinate(double xz) {
|
||||
assert Chunk.CHUNK_SIZE_X == Chunk.CHUNK_SIZE_Z;
|
||||
return Math.floorDiv((int) Math.floor(xz), Chunk.CHUNK_SIZE_X);
|
||||
// Assume chunk horizontal size being 16 (4 bits)
|
||||
return (int) Math.floor(xz) >> 4;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,23 +150,27 @@ public final class ChunkUtils {
|
||||
/**
|
||||
* Gets the chunks in range of a position.
|
||||
*
|
||||
* @param point the initial point
|
||||
* @param range how far should it retrieves chunk
|
||||
* @param chunkX the initial chunk X
|
||||
* @param chunkZ the initial chunk Z
|
||||
* @param range how far should it retrieves chunk
|
||||
* @return an array containing chunks index
|
||||
*/
|
||||
public static long @NotNull [] getChunksInRange(@NotNull Point point, int range) {
|
||||
final int chunkX = point.chunkX();
|
||||
final int chunkZ = point.chunkZ();
|
||||
public static long @NotNull [] getChunksInRange(int chunkX, int chunkZ, int range) {
|
||||
// FIXME: currently broken using GraalVM
|
||||
long[] array = new long[MathUtils.square(range * 2 + 1)];
|
||||
int i = 0;
|
||||
for (int z = -range; z <= range; ++z) {
|
||||
for (int x = -range; x <= range; ++x) {
|
||||
for (int x = -range; x <= range; ++x) {
|
||||
for (int z = -range; z <= range; ++z) {
|
||||
array[i++] = getChunkIndex(chunkX + x, chunkZ + z);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
public static long @NotNull [] getChunksInRange(@NotNull Point point, int range) {
|
||||
return getChunksInRange(point.chunkX(), point.chunkZ(), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the block index of a position.
|
||||
*
|
||||
@ -198,44 +196,12 @@ public final class ChunkUtils {
|
||||
* @return the instance position of the block located in {@code index}
|
||||
*/
|
||||
public static @NotNull Point getBlockPosition(int index, int chunkX, int chunkZ) {
|
||||
final int x = blockIndexToPositionX(index, chunkX);
|
||||
final int y = blockIndexToPositionY(index);
|
||||
final int z = blockIndexToPositionZ(index, chunkZ);
|
||||
final int x = blockIndexToChunkPositionX(index) + Chunk.CHUNK_SIZE_X * chunkX;
|
||||
final int y = index >>> 4 & 0xFF;
|
||||
final int z = blockIndexToChunkPositionZ(index) + Chunk.CHUNK_SIZE_Z * chunkZ;
|
||||
return new Vec(x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a block chunk index to its instance position X.
|
||||
*
|
||||
* @param index the block chunk index from {@link #getBlockIndex(int, int, int)}
|
||||
* @param chunkX the chunk X
|
||||
* @return the X coordinate of the block index
|
||||
*/
|
||||
public static int blockIndexToPositionX(int index, int chunkX) {
|
||||
return blockIndexToChunkPositionX(index) + Chunk.CHUNK_SIZE_X * chunkX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a block chunk index to its instance position Y.
|
||||
*
|
||||
* @param index the block chunk index from {@link #getBlockIndex(int, int, int)}
|
||||
* @return the Y coordinate of the block index
|
||||
*/
|
||||
public static int blockIndexToPositionY(int index) {
|
||||
return (index >>> 4 & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a block chunk index to its instance position Z.
|
||||
*
|
||||
* @param index the block chunk index from {@link #getBlockIndex(int, int, int)}
|
||||
* @param chunkZ the chunk Z
|
||||
* @return the Z coordinate of the block index
|
||||
*/
|
||||
public static int blockIndexToPositionZ(int index, int chunkZ) {
|
||||
return blockIndexToChunkPositionZ(index) + Chunk.CHUNK_SIZE_Z * chunkZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a block index to a chunk position X.
|
||||
*
|
||||
|
@ -14,7 +14,6 @@ import java.util.function.Consumer;
|
||||
public final class EntityUtils {
|
||||
|
||||
private EntityUtils() {
|
||||
|
||||
}
|
||||
|
||||
public static void forEachRange(@NotNull Instance instance, @NotNull Point point,
|
||||
|
Loading…
Reference in New Issue
Block a user