Merge upstream

This commit is contained in:
Moulberry 2021-09-13 15:07:11 +08:00
commit 9531bd5cb3
29 changed files with 615 additions and 816 deletions

View File

@ -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);
}
/**

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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!");
}
}

View File

@ -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);
}

View File

@ -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.
*

View File

@ -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);

View File

@ -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());
}
/**

View File

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

View File

@ -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
*/

View File

@ -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;
}
}
}

View File

@ -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);
}
/**

View File

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

View File

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

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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() {

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -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.
*

View File

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